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.
Ownership
Section titled “Ownership”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.
Live board and round proofs
Section titled “Live board and round proofs”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:
pnpm nx run rennet-server:real-roundsreal-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:
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 normalized session
Section titled “The normalized session”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.
Threads and utility turns
Section titled “Threads and utility turns”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.
Compaction, surfaced not estimated
Section titled “Compaction, surfaced not estimated”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.
Claude Code
Section titled “Claude Code”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:
initializeinitializedthread/startturn/startitem/* notificationsturn/completedturn/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.
Discovery follows the project locus
Section titled “Discovery follows the project locus”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.
Capabilities require evidence
Section titled “Capabilities require evidence”Each capability has three layers:
| Layer | Meaning |
|---|---|
implementedByAdapter | Rennet contains the native-to-normalized mapping |
advertisedByHarness | The installed harness version reports support |
availableInSession | A 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.
Authentication, usage, and egress
Section titled “Authentication, usage, and egress”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.
Code map
Section titled “Code map”| Concern | Owner |
|---|---|
| Port, events, errors, health, and capability types | packages/core/src/harness.ts |
| Shared conformance checks | packages/core/src/harness-conformance.ts |
| Binary discovery | packages/adapters/src/harness-discovery.ts |
| Claude adapter and query integration | packages/adapters/src/claude-adapter.ts, packages/adapters/src/claude-query.ts |
| Codex adapter and app-server transport | packages/adapters/src/codex-adapter.ts, packages/adapters/src/codex-app-server.ts |
| Per-project harness composition | packages/server/src/create-server.ts |
| Client-to-daemon connection | packages/client/src/ws-bridge.ts |
See hand off and the exits for the write-enabled consumer and model council for job assignment.