Architecture

packages/
  engine/    @oharness/engine — the harness; does not know what drives it
  server/    @oharness/server — browser frontend, HTTP + SSE; deployable alone
  sync/      @oharness/sync   — optional transcript sync server
  cli/       oharness         — terminal and headless frontends, all four bins

Dependencies run one way: a frontend consumes the engine, never the reverse. Since the split into packages, the resolver enforces most of that on its own — an undeclared import simply fails — but a relative path that climbs out of one package into another would still resolve, so the layering test also checks for that. The engine contains no console.*, no process.stdout, and never touches the TTY. When it needs to ask the user something it calls an injected permissionPrompt; when it has progress to report it yields an AgentEvent. Who renders is the frontend's business.

The boundary is enforced

A layering convention that lives only in a document lasts until the first person in a hurry. test/layering.test.ts reads the actual source:

AssertionWhy
The engine imports no frontendA reverse dependency ends embeddability, and stops a second frontend being addable
The engine never writes to stdoutstdout belongs to whatever is rendering
Only the logger writes to stderrDiagnostics on the conventional channel, through one seam a frontend can redirect
The engine never touches stdin or readlineA scheduled run has nobody at the keyboard
The library entry exports no frontendAn embedding app should not be dragged into a terminal UI

Neutral history

Session transcripts are stored in the shape defined in packages/engine/src/core/types.ts, not in any vendor's format. One detail earns its place: a tool result carries toolName as well as toolCallId, because Gemini keys tool results by function name rather than by call id.

Capability flags are load-bearing

Models disagree about reasoning parameters, and sending the wrong shape is a 400, not a degradation. So the differences are data on ModelSpec: thinkingMode, effortLevels, maxEffortWithThinkingDisabled, supportsSampling. The adapter dispatches on them per request.

Compaction does not destroy

When the transcript is summarised, what was dropped is appended to an archive alongside the session. The recall tool searches it, so a compacted detail is retrievable rather than gone. Compaction also never splits a tool call from its result — a transcript that does is rejected by every provider.