Skip to content

Harness adapters

Rennet runs the coding harnesses already installed for the repository’s execution locus. The adapter boundary gives core review code one session model for Claude Code and Codex without copying provider credentials or bundling a Rennet harness executable.

HarnessPort and its event types live in packages/core/src/harness.ts. The interface has no Node or Electron dependency. @rennet/adapters owns binary discovery, process and SDK integration, native-frame decoding, and provider errors. @rennet/server discovers and memoizes harnesses for each project locus, then injects the selected adapter into core jobs.

The desktop and browser shells reach the daemon through @rennet/client. The React app does not spawn harnesses, and the Electron main process does not own model routing.

Scripted owner-loop proof stays outside production

Section titled “Scripted owner-loop proof stays outside production”

The owner-loop integration proof uses a lane-unique JSON plan to implement a HarnessPort in tests. The plan is schema-validated before the test server is created, writes one JSONL invocation record per turn, and applies coding edits as exact replacements under SessionSpec.cwd. Reloading the plan reloads the ledger, so a restarted daemon cannot replay a consumed coding step.

This is a test composition, not a daemon mode. The production daemon never reads the plan and has no environment variable or command-line flag that selects canned harness responses. The Electron journey starts an E2E-owned daemon process, injects the test port through RennetServerOptions.testHarnessPort, and then launches the unchanged desktop client against the daemon claim. The client still uses the real WebSocket protocol, persistence stores, capture path, board runtime, round coordinator, and restart behavior. The spec is committed as a launched journey but is not part of the browser-free local gate.

Two opt-in checks exercise installed harnesses. Terminal accounting alone is not a functional pass. Sequence must contain a reachable order_step. Decisions and Flagged must contain a reachable decision or finding, or return the exact no-decisions or no-findings typed absence. Prose-only boards, empty sections, detached typed elements, Design, and Noise cannot satisfy those core checks. The server check requires both Claude Code and Codex, records the start and terminal result of each provider call, and runs the real round-report and five-lens pipeline. The command streams timestamped round progress plus provider-call start and terminal records directly to the invoking terminal. A watchdog aborts the provider turns one minute before the outer test timeout so a stalled run leaves its last lane and in-flight call in the log:

Terminal window
pnpm nx run rennet-server:real-rounds

real-rounds is uncached because its verdict depends on installed harnesses and provider responses. The launched desktop command counts as owner-journey proof only when it exits green. Its initial, reconstructed, and successor generations must all pass the same reachable core oracle above. It records the first healthy daemon PID, closes the app-owned daemon, proves that process and its claim are gone, and requires the relaunched app to use a different healthy PID. After board.read reconstructs the same results, the check stages an exact source edit, dispatches a real coding round, and mounts the live /run route. It waits for durable Return to navigate back to the review automatically, then uses the visible View the New Boards control. Without reloading the page, the final assertions read the changed fixture, round diff and commit range, successor patchset, new generation, and visible successor terminal results: reachable Sequence material plus reachable Decisions and Flagged material or their exact typed absences:

Terminal window
RENNET_LIVE_E2E=1 pnpm nx run rennet-desktop:e2e --args="board-drafting-live"

Both commands can send the reviewed material to the configured harness provider. They stay outside pnpm check and must run only against an authorised repository.

The current port exposes descriptor and health information plus one operation: createSession(spec). A session then provides one event stream and the send, interrupt, and close methods. SessionSpec now carries an optional resume?: { harnessSessionId: string }: a spec with it continues a prior harness conversation, one without it starts fresh. Fork is still not part of the interface.

Every event carries the same envelope:

interface HarnessEventBase {
seq: number
harness: "claude-code" | "codex"
sessionId: string
turnId: string | null
receivedAt: number
native: unknown
}

The adapter assigns the monotonically increasing seq. It also retains the native frame. Known frames become session, text, tool, error, or metered-key events. Unknown frames become passthrough events rather than disappearing.

Tool results keep both structured output and readable text. Terminal outcomes are a discriminated union of completed, cancelled, and failed, so missing usage or structured output cannot be mistaken for a successful empty value. Codex output schemas are normalized at the transport boundary to its strict structured-output subset: object fields become required and nullable, object extras are closed, and Zod’s unsupported oneOf projection becomes anyOf for generation. A root union of object outcomes becomes one required-nullable object envelope, then emitted null fields are removed. Board jobs still parse the result through their original schema in core, so this normalization cannot weaken an accepted board.

Interactive chat, board seats, and coding rounds run on persistent threads in Rennet’s T3 Code sidecar. The sidecar owns provider conversation state and turn execution. A coding round gets its own thread in the session’s bound workspace and returns the sidecar’s checkpoint.

The round runtime serializes work per session and pins the selected provider. A later round uses that provider or reports why it cannot run; it does not silently switch providers.

Rennet’s own utility turns do not resume either, by construction rather than by omission: the project scout, the repo map, the delta digest and their kind run as ephemeral sessions, which the Claude adapter maps to the SDK’s persistSession: false, and a session that was never persisted has no transcript to resume. Each carries everything it needs and is re-billed in full on a retry, which is why their prompts name paths instead of holding content.

The project scout is one such turn, and it fills two different kinds of gap. Most of its answers are facts determinism either found or did not, so the seat is asked only for the ones left empty. The project’s logo is not a fact but a judgement — which image best identifies this project — so the seat is asked for it on every run, even when the deterministic pass already found a file. What it receives is an inventory, not images: a bounded walk of the repository yields at most 20 candidate paths, with a truncation marker stating how many it did not list, and the prompt says what a suitable mark is (a standalone square mark over a wordmark, vector over raster, no sponsor logos, no screenshots) and that the list is a starting point rather than a fence. A path the seat returns is validated against the checkout — it must resolve to a real file, of an accepted image type, inside the repository — before it displaces determinism’s own best guess.

The board seats are the other shape, and they do not run on these adapters at all: a lens seat, the Flagged lane’s two provider seats and the round report are persistent threads in the T3 sidecar, one per seat per generation. A repair there is the next turn on the thread that already holds the base prompt and the failing draft, so it carries only the lint pointers and the frozen ids. There is no ephemeral board leg to fall back to — a generation with no sidecar drafts no board and says why. See the T3 Code sidecar.

When the harness compacts its own context, Rennet shows that it happened rather than hiding or guessing it. A harness compaction event becomes exactly one compact_boundary row in the turn stream. The row carries the harness’s own structured compact_metadata — the trigger (manual or auto) and its pre/post token counts, each present only when the harness reported it, never a substituted zero. The live SDK frame carries no free-text summary (the compacted conversation summary goes to the CLI’s own transcript, which the live stream filters out), so Rennet forwards the structured metadata verbatim and never attributes a fabricated prose sentence to the harness.

The context meter follows the same rule: it asks, it does not estimate. It reports only what the harness states about its context window, and is absent — not zero, not a computed percentage — when the harness gives no figure. An invented budget would be a lie in the UI; an honest gap is the truth.

The Claude adapter uses @anthropic-ai/claude-agent-sdk and sets pathToClaudeCodeExecutable to the discovered claude binary. Packaging removes the SDK’s bundled platform executables. The resulting app uses the user’s installed CLI and its existing authentication.

The query adapter passes the child environment explicitly because the SDK’s env option replaces rather than merges it. Session frames are normalized in packages/adapters/src/claude-adapter.ts.

Claude sessions can accept an output schema and an optional tool list. The coding-agent handoff does not narrow the default tool set, so the agent can edit files and run project commands. Read-oriented review jobs may supply a smaller tool set for that workload.

The Codex adapter speaks newline-delimited JSON-RPC with codex app-server. packages/adapters/src/codex-app-server.ts owns the wire protocol, and codex-turn-transport.ts exposes it to CodexAdapter.

One turn-scoped child runs this sequence:

initialize
initialized
thread/start
turn/start
item/* notifications
turn/completed

turn/start carries the prompt, repository working directory, model, sandbox policy, approval policy, and optional output schema. Structured output returns through the app-server protocol. The adapter does not use an output scratch file for this path.

The same agentic port runs write-enabled work-order rounds when Codex is the session’s selected harness. It receives the full-access sandbox and never-ask approval posture used by the acting path, so it can edit, run the configured gate, and return checkpoint-measured changes. The durable worker and round receipts record the exact Codex version that executed the turn.

The transport maps assistant deltas, completed messages, tool lifecycle events, token usage, failures, and interrupts into HarnessEvent. It terminates the turn child after the terminal event. Codex does not report per-turn dollar cost through this integration, so the capability remains absent.

For a WSL project, discovery and process execution run inside the selected distro and pass a distro-native working directory. The Codex app-server reference records the full method mapping and discovery candidates. The Windows and WSL guide covers the user-facing setup.

A graphical app may inherit a different PATH from an interactive shell, and a shell command may resolve to a function rather than an executable. Discovery therefore reads the login-shell path, combines it with the process path and known install locations, probes absolute executable candidates, and records the chosen binary and health result.

Codex discovery includes the executable bundled in ChatGPT on macOS after user-installed candidates. WSL discovery searches inside the selected distro. RENNET_DISABLE_HARNESS=1 disables discovery for hermetic tests.

Discovery proves that a candidate can start and answer its probe. It does not read the harness’s credential files.

Each capability has three layers:

LayerMeaning
implementedByAdapterRennet contains the native-to-normalized mapping
advertisedByHarnessThe installed harness version reports support
availableInSessionA live session demonstrated the behavior

buildCapabilities() starts every layer at false and turns on only the entries supplied by a passing check. The shared conformance suite in packages/core/src/harness-conformance.ts drives the same named behaviors through any HarnessPort. Hermetic tests can prove adapter implementation. Real runs are required for the outer layers and for the recorded testedRange.

The harness authenticates itself. Rennet does not copy OAuth tokens or API keys into an adapter. Claude’s session-start frame reports apiKeySource; metered key sources produce a visible warning without stopping the turn.

The T3 Code sidecar drives the same installed claude and codex through T3 Code’s own provider layer, seeded with the absolute paths this discovery resolves; lens seats stay on the adapters described here.

Usage is optional on a normalized session outcome. An adapter omits it when the harness supplied no token record. RSP documents require a token block, and the current document producers substitute an all-zero block when a model turn has no usage record. Those zeros therefore do not prove that the turn consumed no tokens. Provider-reported and derived dollar values remain separate and stay null when no amount is available.

The lens pipeline’s seat turns feed a second, fuller tap: the turn-metrics collector reads usage and total_cost_usd off the Claude result frame’s raw native record, and the Codex seat maps its token record onto the same shape with no price. The generation sums that collector into its durable usage, so what a review cost is a number on the round rather than a figure parsed and dropped.

There is no hosted Rennet backend. The daemon starts local harness processes on a loopback transport where a subprocess needs MCP access. Review context still reaches the provider used by the selected harness.

ConcernOwner
Port, events, errors, health, and capability typespackages/core/src/harness.ts
Shared conformance checkspackages/core/src/harness-conformance.ts
Binary discoverypackages/adapters/src/harness-discovery.ts
Claude adapter and query integrationpackages/adapters/src/claude-adapter.ts, packages/adapters/src/claude-query.ts
Codex adapter and app-server transportpackages/adapters/src/codex-adapter.ts, packages/adapters/src/codex-app-server.ts
Per-project harness compositionpackages/server/src/create-server.ts
Client-to-daemon connectionpackages/client/src/ws-bridge.ts

See hand off and the exits for the write-enabled consumer and model council for job assignment.