Harness

May 5, 2026 · View on GitHub

A one-page block diagram + data flow. For deeper detail per layer see the per-service wiki pages.


Block diagram

+----------------------------------------------------------------------+
|  Harness.app                       Swift 6 / SwiftUI / non-sandboxed |
|                                                                      |
|  +----------------+  +-------------------+  +--------------------+   |
|  | GoalInputView  |  | RunSessionView    |  | RunHistoryView     |   |
|  | (compose)      |  | (live)            |  | (past runs)        |   |
|  +-------+--------+  +---------+---------+  +---------+----------+   |
|          |                     |                       |             |
|          v                     v                       v             |
|        AppCoordinator (@Observable, navigation only)                 |
|                       |                                              |
|                       v                                              |
|        AppState (@Observable, cross-section app-level state)         |
|                       |                                              |
|                       v                                              |
|  +------------------------------------------------------------------+|
|  |  RunCoordinator (actor) — orchestrates one run                   ||
|  |    build → boot → install → launch → loop → log → cleanup        ||
|  +-+---------------+----------------+--------------+----------------+|
|    |               |                |              |                 |
|    v               v                v              v                 |
|  +---------+  +-----------+  +-------------+  +-----------+          |
|  | XcodeBuilder|SimulatorDriver|  AgentLoop |  RunLogger | RunHistoryStore|
|  | (xcodebuild)|(simctl + idb) |  (loop)    |  (JSONL)   | (SwiftData)    |
|  +------+---+  +-------+-------+  +----+----+  +----+----+ +-----+----+   |
|         |             |                |              |        |         |
|         |             |                v              |        |         |
|         |             |        +-------+--------+     |        |         |
|         |             |        | ClaudeClient   |     |        |         |
|         |             |        | (Anthropic SDK)|     |        |         |
|         |             |        +-------+--------+     |        |         |
|         |             |                |              |        |         |
|         v             v                v              v        v         |
|  +----------------------------------+   +----------------------------+   |
|  |  ProcessRunner (actor)           |   |  Filesystem                |   |
|  |  the only owner of Process()     |   |  ~/Library/Application     |   |
|  +-+--------------+-----------------+   |  Support/Harness/runs/<id>/|   |
|    |              |                     +----------------------------+   |
|    v              v                                                       |
| `xcodebuild`   `xcrun simctl` / `idb` / `idb_companion`                   |
+----------------------------------------------------------------------+

Data flow per run

  1. Compose — User enters project + scheme + simulator + persona + goal + mode in GoalInputView. The view-model assembles a GoalRequest.
  2. Start — User clicks Start. RunSessionViewModel calls RunCoordinator.run(_:) which returns an AsyncThrowingStream<RunEvent, Error>.
  3. Build — Coordinator invokes XcodeBuilder.build(...) → spawns xcodebuild via ProcessRunner with derived data isolated under the run dir → returns the .app bundle URL.
  4. Sim setupSimulatorDriver.boot(...), install(_:), launch(bundleID:). Status bar overrides applied.
  5. LoopAgentLoop runs (per 13-agent-loop.md):
    • Screenshot via SimulatorDriver
    • ClaudeClient.step(...) → tool call
    • In step mode, the coordinator awaits user approval via AsyncStream<UserApproval> injected from the view-model
    • Execute tool via SimulatorDriver
    • RunLogger appends events
    • Loop until mark_goal_done / cancel / budget
  6. Wrap-up — Final run_completed row written. meta.json written. SwiftData RunRecord saved by RunHistoryStore. Coordinator emits a final RunEvent.completed(verdict:) and the stream finishes.
  7. Replay — User opens history, double-clicks a row. RunReplayView loads events.jsonl + meta.json from the run directory. The view scrubs through steps with full reasoning visible.

State ownership

ConcernOwnerScope
Navigation (sidebar selection, sheets, modal flags)AppCoordinatorApp lifetime
App-level cross-section state (API key presence, idb health, default sim)AppStateApp lifetime
Per-run state (live screenshot, step feed, approval pending)RunSessionViewModelOne run
Run orchestration (build/install/loop/log)RunCoordinator (actor)One run
Per-run history recordRunRecord (SwiftData)Persisted
Per-step eventsevents.jsonl on diskPersisted
Recently-used Xcode projectsProjectRef (SwiftData)Persisted

No singletons except ToolLocator (paths to external CLIs) and the keychain accessor (cached API key). Everything else is injected.


Concurrency model

  • @MainActor by default in views and view-models.
  • Actors for RunCoordinator, ProcessRunner, RunLogger, RunHistoryStore, ClaudeClient. Reading/writing their state from @MainActor always goes through await.
  • Task.detached for filesystem reads triggered from view bodies (none allowed in production, but lifecycle-bound load methods may use it).
  • AsyncThrowingStream for run events, screenshot frames, and process streaming output.
  • Cancellation propagates: cancelling the run task cancels the coordinator; the coordinator cancels its child tasks (loop, screenshot poller, in-flight Claude call). ProcessRunner catches cancellation and SIGTERMs the child process.

What lives where (lookup table)

ConcernLocation
App entry / @mainHarness/App/HarnessApp.swift
Navigation stateHarness/App/AppCoordinator.swift
App-level stateHarness/App/AppState.swift
Path constantsHarness/Core/HarnessPaths.swift
Domain models (Run, Step, Action, Friction, Verdict)Harness/Core/Models.swift
Tool schemaHarness/Tools/AgentTools.swift
The loopHarness/Domain/AgentLoop.swift
Run orchestrationHarness/Domain/RunCoordinator.swift
Subprocess actorHarness/Services/ProcessRunner.swift
External CLI discoveryHarness/Services/ToolLocator.swift
xcodebuild wrapperHarness/Services/XcodeBuilder.swift
simctl + idb wrapperHarness/Services/SimulatorDriver.swift
Anthropic SDK wrapperHarness/Services/ClaudeClient.swift
JSONL writerHarness/Services/RunLogger.swift
SwiftData indexHarness/Services/RunHistoryStore.swift
Keychain wrapperHarness/Services/KeychainStore.swift
First-run wizardHarness/App/FirstRunWizard.swift
Settings sheetHarness/Features/Settings/
Goal inputHarness/Features/GoalInput/
Live run viewHarness/Features/RunSession/
HistoryHarness/Features/RunHistory/
ReplayHarness/Features/RunReplay/
Friction reportHarness/Features/FrictionReport/
Design tokens + primitivesHarnessDesign/ (separate package)
Prompt librarydocs/PROMPTS/ (loaded as bundle resources)

Cross-cutting

  • Logging. Every type uses os.Logger with subsystem com.harness.app. No print() outside previews.
  • Error surface. Typed errors per layer (ProcessFailure, BuildFailure, SimulatorError, ClaudeError, LogWriteFailure). The view-model layer maps these to user-facing messages.
  • Testing. Every protocol has a mock; the agent loop has replay-based fixtures. See standards/10-testing.md.

For per-component depth, see [Core-Services](../wiki/Core-Services.md).