Skip to content

Spec: M3 — First Harness (Claude Code) #107

Description

@Sandstorm831

Milestone: #106 (M3: First Harness — Claude Code). Spec for the /to-tickets cut. ADRs at HEAD and the spine amendments control where this text differs.

Problem Statement

Secant can only run Command-only Workflow Bundles. A Bundle that carries an Agent step, an Interactive agent step, or an authored Human Gate is refused at Preflight with "no headless execution in this release", so the Proof Bundle (Test Repair) has never run. A user who has Claude Code installed and logged in cannot route any Step through it. Nothing pauses a Run when the agent wants to edit a file or run a command, nothing lets a human steer or stop a live Turn, and no conversation with a Harness is ever recorded, replayed, or shown in either client. CI therefore proves nothing about agent Steps, and every claim about "works with Claude Code" rests on someone's laptop.

Solution

A user launches the Proof Bundle from the TUI or the headless client against the Claude Code they already have on PATH. Secant finds the executable, qualifies it, and spawns it as an owned child process speaking stream-json over stdio. The agent's Turn streams into the Workbench timeline as it happens: assistant text, tool use, file edits, commands. When Claude Code wants permission for a tool, the Run pauses on an approval request that the user answers in the Workbench (or that a headless flag answers by declared policy); the answer is relayed and the Turn continues. A free-text Human Gate asks a question and stores the answer as a text Run Artifact. The user can interrupt a live Turn; Secant stops the process truthfully, rests the Run halted, and resumes the same Claude Code Session later. In the TUI only, an Interactive agent step hands the Session to the human for turn-taking and ends only when the human presses End Step. All of this is exercised in CI on three operating systems against a fake claude that replays byte-faithful recordings of the real one, and the recordings say exactly which Claude Code produced them.

User Stories

Bundle author

  1. As a Bundle author, I want an agent Step with a prompt asset and a named session to run one autonomous Turn in Claude Code, so that my Bundle can delegate a repair to an agent without me writing any Harness-specific code.
  2. As a Bundle author, I want {{artifact:name}} prompt slots filled from the Run's bindings, with a file artifact rendered as a path Claude Code can open, so that handoff between Steps is by name and I never bake @ or any Harness syntax into a prompt.
  3. As a Bundle author, I want a skill asset listed in a Step's uses to reach the agent as a plain directory path with a SKILL.md, so that maintainer-supplied skills work unaltered on the first Harness.
  4. As a Bundle author, I want a human-gate Step with shape free-text whose answer becomes a text Run Artifact, so that a later Step can read what the human wrote.
  5. As a Bundle author, I want a human-gate Step with shape approve-reject to run as an authored Step (not only as a Review checkpoint), so that the Proof Bundle's approve-commit gate actually gates the commit.
  6. As a Bundle author, I want an interactive-agent Step that hands its named Session to the human, so that a grill-style conversation can precede an autonomous Step in the same Session.
  7. As a Bundle author, I want the same named Session reused by successive Agent Steps and Iterations, so that the agent keeps its context across the repair loop.
  8. As a Bundle author, I want fresh as a Session name to mean a new Session per Step Attempt (per Iteration inside a Repeat group), so that I can choose isolation explicitly.
  9. As a Bundle author, I want my Bundle refused at Preflight, before a Run exists, when the selected Harness cannot meet a Step kind's capability needs, so that I never discover a gap mid-Run.
  10. As a Bundle author, I want never to select or constrain a model in a Bundle, so that the user's own Claude Code configuration decides.

Run operator, TUI

  1. As a TUI user, I want to launch the Proof Bundle and see the agent's Turn stream into the Workbench timeline (assistant text, tool calls, file edits, commands, subagents) as it happens, so that I know what the agent is doing.
  2. As a TUI user, I want replaceable preview text while the assistant is still typing, replaced by the authoritative final content when the Turn ends, so that the timeline never shows a half-message as truth.
  3. As a TUI user, I want a Claude Code permission prompt to appear as an approval request naming the exact tool and input, with the exact decisions Claude Code offers, so that I approve the real action and not a paraphrase.
  4. As a TUI user, I want to answer an approval request from the Workbench and see the Turn continue, so that the Run pauses only as long as I take to decide.
  5. As a TUI user, I want the approval request to disappear when the Turn ends, is interrupted, or is lost, and never to be re-asked on Claude Code's behalf, so that I never answer a question the agent is no longer asking.
  6. As a TUI user, I want a blocked Run to say whether it is waiting on a durable Human Gate or on an ephemeral Harness Request, so that I know whether it survives me closing Secant.
  7. As a TUI user, I want to answer a free-text Human Gate by typing in the Workbench, so that I can give the agent the information it asked for.
  8. As a TUI user, I want an Interrupt control on a live Turn that stops Claude Code's work, ends the Step Attempt cancelled, and rests the Run halted, so that I can stop a bad run without cancelling the whole Run.
  9. As a TUI user, I want Steer to be shown as unavailable with the reason "Claude Code has no same-Turn steer", so that I am never offered a control that would be faked.
  10. As a TUI user, I want to resume a halted Run and have the agent continue in the same Claude Code Session, so that interruption costs me nothing but time.
  11. As a TUI user, I want an Interactive agent step to hand me the prompt input, show each of my Turns and the agent's replies in the timeline, and end only when I press the End Step control and confirm, so that the routing advances only on my explicit decision.
  12. As a TUI user, I want the End Step control to be offered only at a Turn boundary, so that I cannot end a Step while the agent is mid-Turn.
  13. As a TUI user, I want the Workbench header to show which Claude Code (version, executable) and which model the current Attempt is running under, so that I can trust or question the evidence.
  14. As a TUI user, I want to see the agent's context-window pressure and usage when Claude Code reports them, and nothing invented when it does not, so that I know when a Session is filling up.
  15. As a TUI user, I want a Session transcript readable from the Workbench (exact Turn inputs, authoritative assistant content), so that I can review what was said after the fact.
  16. As a TUI user, I want the timeline to keep following the live edge while I stay at the bottom and to stop following when I scroll up, with a jump-to-latest count, so that I never lose my place.
  17. As a TUI user, I want colour never to be the only signal distinguishing a Human Gate, a Harness Request, an interactive Turn, and an agent Turn, so that the Workbench reads correctly on any terminal.
  18. As a TUI user, I want a Run whose Claude Code is not logged in to fail with "Authentication required for Claude Code. Log in separately through Claude Code, then retry.", so that I know exactly what to do.
  19. As a TUI user, I want a Run whose Claude Code cannot be found to fail at Preflight naming what was searched (configured command, then PATH), so that I can fix my environment.

Run operator, headless

  1. As a headless user, I want run launch of the Proof Bundle to follow the live Run and exit 0 on succeeded, 2 on blocked, 1 otherwise, exactly as today, so that CI scripts do not change.
  2. As a headless user, I want a flag on run launch and run resume declaring how approval Harness Requests are answered (allow or deny, default deny), so that an unattended Run can proceed through tool approvals by a policy I chose.
  3. As a headless user, I want each policy-answered Harness Request recorded in the Run's timeline as answered by client policy, so that a reviewer can see every approval that was granted without a human.
  4. As a headless user, I want a Run that reaches a free-text Human Gate to rest at the gate, exit 2, and name the follow-up command, so that a later invocation can answer with run answer --text.
  5. As a headless user, I want run answer to accept free text for a free-text gate and continue the Run in that process, so that headless gates of both shapes work the same way.
  6. As a headless user, I want a Bundle carrying an Interactive agent step refused at Preflight with the remediation "run this Bundle in the TUI", so that headless v1 never pretends to relay turns it cannot.
  7. As a headless user, I want run show to print the Turn timeline, including tool calls, requests, and the Attempt's effective model, so that a Run is inspectable without the TUI.
  8. As a headless user, I want the --json shapes that CI already parses to stay frozen and new fields to be additive, so that existing scripts keep working.
  9. As a headless user, I want a Ctrl+C during a live Turn to stop Claude Code and rest the Run halted (not cancelled), so that I can resume later.

Maintainer recording and replaying

  1. As a maintainer, I want an opt-in recording tool that runs a named scenario against my installed Claude Code and writes a case directory with byte-faithful stdio, a provenance sidecar, per-Turn Workspace patches, and the bridge calls made, so that CI can replay exactly what the real Harness did.
  2. As a maintainer, I want the sidecar to name harness, executableVersion, protocolVersion, recordedAt, redactions, and refreshCommand, so that anyone can tell which Claude Code produced a recording and how to refresh it.
  3. As a maintainer, I want the recorder to redact my home directory, user name, and any Secant-introduced secret (the bridge token) and to refuse to write a recording containing a credential pattern, so that recordings are safe to commit.
  4. As a maintainer, I want the fake claude to replay a case over stdio, waiting for each stdin Turn before emitting that Turn's output, applying the Turn's Workspace patch, and performing the recorded bridge call and waiting for its answer, so that the deterministic suite exercises the whole path including approvals.
  5. As a maintainer, I want the fake dropped on a temporary PATH under the name claude (with an npm-style .cmd shim on Windows), so that real spawning and shim resolution are exercised on every OS.
  6. As a maintainer, I want the guidance-structure suite to fail a case directory without its sidecar, so that provenance is never optional.
  7. As a maintainer, I want the implementing issue of any re-record to state what changed semantically, so that protocol drift is visible in the tracker.

CI and reviewer

  1. As a reviewer, I want the deterministic suite to run the Test Repair Bundle end to end through the headless client against the replayer on all three OSes, so that condition 1, 2, and 4 of the genericity proof are green in CI.
  2. As a reviewer, I want the package smoke to run one headless Proof Bundle Run from the installed Secant binary against the same fake, so that the shipped artifact is what is proven.
  3. As a reviewer, I want to point at the single seam where each new capability landed (the step-kind dispatch table row, the Harness Adapter Interface, the Projection Port Operation), so that condition 4 is verifiable and nothing branches on Bundle identity.
  4. As a reviewer, I want no real Harness ever executed in CI, so that the gate is credential-free and deterministic.
  5. As a reviewer, I want every Harness-native dependency confined to the Harness Module and re-earned under the dependency yardstick, so that the rest of the tree stays protocol-free.
  6. As a reviewer, I want the Matt front Bundle to run in the TUI against the fake through an in-memory renderer test, so that the Interactive agent step is observed without breaking the Proof Bundle's difference contract.

Implementation Decisions

Module topology

  • New process Module (target source, imports nothing). It owns the "owned child process" mechanics both Command steps and Harnesses share: the single PATH walk, Windows npm .cmd/.bat shim resolution to the real interpreter and script, direct spawn (never a shell), pipe stdio, detached process groups off Windows, SIGTERM→SIGKILL escalation with taskkill /T /F on Windows, and process-tree reaping. The existing resolver moves here from Run execution unchanged in behaviour; execution, application, and harness import it. The module policy table gains the row and the import arrows; the deletion test holds because removing it would redistribute shim parsing and kill escalation into two callers.
  • The harness Module is created by the first slice that needs it (no scaffolding). It imports process only. It owns: the Harness Adapter Interface (ADR 0022 shapes), the Claude Code Adapter, the stream-json protocol model, discovery and qualification, the MCP permission bridge, and a Harness-local AGENTS.md listing its invariants. Harness-native dependencies live here and nowhere else.
  • Run execution gains three rows in its closed step-kind dispatch table: agent, interactive-agent, and human-gate. The scheduler still never branches on kind or Bundle identity; each row is the named seam for its capability. The Step context grows a prepared Harness handle, a durable Turn recorder, and a request-answer channel.
  • Run Store gains Harness Session, Turn, Turn event, and transcript records (schema below) through the drizzle migration flow, plus a pending authored Human Gate record.
  • Application extends the run Projection with a live overlay and the new Operations and Action Offers, and extends Preflight with Harness discovery, qualification, and the capability-need union check.
  • Headless and TUI consume only the Projection Port.
  • Tests gain a harness test domain owning the recording tool, the replayer, the deterministic fake Adapter, the conformance suite, and the fixture tree the testing guidance already names.

Discovery, qualification, and the profile

  • Discovery order per Choose Crucible's process, runtime, packaging, and distribution architecture #21: an explicit configured command or path first, then the canonical PATH name claude. The explicit form in M3 is one environment variable naming the executable path or command; a settings file is out of scope. Windows .cmd shims resolve to the real target through the process Module and are spawned directly. unsupported-shim and not-found are Preflight Problems naming what was searched.
  • Preflight runs the synchronous half only: the Harness entry's shared discovery helper (configured command, then the canonical PATH name, Windows shims resolved or refused) and the capability-need union against the Harness's served-capability table, so a not-found, unsupported-shim, or unserved-capability refusal lands before a Run exists. prepare runs at execution start, creates no Session, sends no conversational content, and probes claude --version and platform (amended 2026-09-17, Audit: M3 row A13). It produces the evidence-bearing profile: executable path, observed version, shim-vs-native, platform, configuration posture, and Adapter revision. Qualification evidence is cached keyed by executable path, version, and file identity, and requalified on drift.
  • Claude Code profile facts (M3): native recovery reattach by resume-by-id; recovery identifier observable before submission (Secant mints the session UUID and passes it at spawn); Steer unavailable; interruption process-only stop (SIGTERM ends the Turn and the process; the Session stays resumable); approvals available through the Secant-hosted bridge; structured clarifications unavailable (Claude Code offers no raw-CLI question callback; never emulated); model selection unavailable to Secant in M3 with effective-model reporting from the init message and result usage; skill delivery plain-path; file delivery plain absolute path. Every fact carries the evidence it rests on.
  • Configuration posture is user-compatible: no --bare, no --strict-mcp-config, no --allowedTools, no --tools, no --model, no permission mode flag. Secant adds only its own stream flags, session id or resume id, its MCP config, and its permission-prompt tool. The user's own settings, hooks, MCP servers, skills, and CLAUDE.md apply and the profile records this posture. Authentication belongs to the user's Claude Code; an authentication failure surfaces as the typed failure "Authentication required for Claude Code. Log in separately through Claude Code, then retry."
  • Ownership of the prepared Harness transfers exactly once from composition, which prepares it when execution starts, to the Run (amended 2026-09-17, Audit: M3 row A13).

Process and Session model

  • Invocation: claude -p --input-format stream-json --output-format stream-json --verbose --include-partial-messages, with --session-id <uuid> for a new Session or --resume <id> for a detached one, --mcp-config <inline json> naming the bridge, and --permission-prompt-tool <bridge tool>. The Workspace is the child's working directory.
  • One owned process per live named Session. The process starts at the Session's first Turn and stays alive with stdin open across Turns of successive Step Attempts and Iterations while the Run is live. Each Turn is one stream-json user message on stdin; the result message is the Turn's authoritative boundary. Session availability after a completed Turn is open while the process lives.
  • The process is closed (stdin end, bounded wait, escalate) when the Run rests (blocked at a Human Gate, halted, failed), when the Session's last Step completes, or on Interrupt. The Session becomes detached with the resume id as its advisory recovery coordinate. The next Turn on a detached Session spawns a new process with --resume; success requires the init message to acknowledge the same session, else availability is unusable and the Attempt fails with a typed recovery failure. Recovery never silently creates a fresh conversation.
  • Startup recovery of a Run whose owner died reconciles Turn state from the Store but starts no process; explicit resume does.
  • Timeouts apply only to launch and init handshake, bridge connection (Claude Code's own MCP startup timeout applies), control acknowledgement, and cleanup. Never to agent thought, tools, approvals, or a whole Turn.
  • The process Module supervises the child: bounded stdin-close, wait, terminate, kill; process tree termination on every OS.

Turn events and results

  • stdout is read as NDJSON with backpressure; stdin writes await the drain signal so frames never interleave. Blank and non-JSON diagnostic lines are dropped; a malformed JSON-looking line is protocol corruption and ends the Turn lost with cause protocol-corruption.
  • Normalization to Crucible events: system/init → Session facts (native session id, effective model, Claude Code version, tools, MCP status); assistant complete messages → authoritative assistant content and tool-use activity; user tool results → tool completion activity; stream_event deltas → replaceable preview only; subagent messages keep parent identity; unknown message types → generic activity, unknown fields ignored. Raw frames, private reasoning, telemetry, and ordinary stderr are never canonical events.
  • Result mapping: result with success subtype → completed; result error subtypes (max turns, budget, execution, structured-output) → failed with the subtype as category; process exit after SIGTERM Secant sent → interrupted; exit without a result for the active Turn → lost with completion-unknown and the exit code; spawn failure or init never seen → not-started. Cost fields are recorded as client-side estimates, never billing.
  • Terminal ordering: receive terminal evidence, publish remaining events and expire requests, close the event producer, settle the result. No event follows the result.
  • Per Attempt the Store records the effective model reported by init.

Controls

  • steer: rejected as unsupported; the profile says so and the Port's offer carries the reason.
  • interrupt: SIGTERM to the process group (taskkill tree on Windows); the Turn enters interrupting, rejects further controls, drains to exit; result interrupted with the profile's process-only stop variant; the Step Attempt ends cancelled; the Run rests halted; the Session is detached. If the process does not exit within the cleanup bound, SIGKILL follows and the result is lost with interruption-unknown.
  • answer-request: matches one exact outstanding request id and shape; races settle as rejected values (expired, already-settled, shape-mismatch), never throws.
  • Ending an Interactive agent step is not a Harness control; it stays above the Seam (below).

MCP permission bridge

  • Secant hosts one Streamable HTTP MCP server per live Run on loopback with a random port and a per-Run bearer token, using @modelcontextprotocol/sdk inside the Harness Module (fenced there by the module policy; zod peer already present; a manual THIRD-PARTY-NOTICES entry accompanies it). The bridge exposes one tool that receives Claude Code's permission prompt (tool name, input, suggestions).
  • Each call becomes an approval Harness Request on the active Turn carrying the exact tool name, exact input, and the offered decisions allow and deny (Claude Code's prompt tool contract offers no "always"; Secant offers none). The tool call blocks until answered, then returns the decision in Claude Code's expected shape (allow with the unchanged input, or deny with a message).
  • Requests are ephemeral and Turn-scoped: several may be outstanding; all expire on completed, interrupted, or lost, and an expired request's pending bridge call returns deny with "request expired". Request lifecycle (raised, answered by whom, expired) is recorded as Turn events; the request itself is live-only and is never recreated after resume.
  • The bridge token is a Secant-introduced secret: redacted from diagnostics, transcripts, and recordings.
  • The bridge is started only for Runs whose routing has a Step kind with capability need agent-turn or interactive-turns.

Agent step

  • Executor row agent: records Turn admission in the Store, renders the prompt, starts one Turn in the Step's named Session (or a fresh one), drains events into the Store and the live overlay, and maps the result: completed → Attempt succeeded; failed → Attempt failed (retryable within budget); not-started → Attempt failed; interrupted → Attempt cancelled, Run halted; lost → Attempt indeterminate, Run halted.
  • The Agent step produces no Artifacts in M3 beyond the transcript entries; the Proof Bundle reads the world through its Command step. Declared produces on an Agent step is a Composition error until a later spec defines publication.
  • Prompt rendering is Adapter-owned: text-like artifacts substitute as canonical text; a file artifact substitutes as the absolute path (Workspace-relative inputs resolved against the Workspace); file-set as one absolute path per line. Each skill in uses appends a line telling the agent to read that skill's SKILL.md at its absolute path in the Run's read-only asset space. No @ syntax is baked in; if a later recording proves print-mode @ expansion, the profile fact changes, not the Bundle.
  • The rendered prompt and its origin (managed) are the Turn's transcript input.

Human Gate as an authored Step

  • Executor row human-gate: records a pending gate (producing Attempt, exact rendered message, shape, declared output Artifact for free-text), rests the Run blocked with basis "durable Human Gate", and returns. answer-human-gate for approve-reject keeps the M2 semantics (reject ends the Run failed). For free-text the answer is published as a text Run Artifact bound to the Step's declared output name, the Attempt settles succeeded, and the Run advances, all in one atomic Store boundary. Gate answers are idempotent on the operation id.
  • Review checkpoints keep their derived-gate path unchanged.

Interactive agent step and End Step

  • Executor row interactive-agent: records the Attempt, rests the Run blocked with basis "interactive Turn", and offers send-interactive-turn and end-interactive-step. Each send-interactive-turn is one Turn with origin human in the Step's Session; the human's text is the transcript input verbatim, Secant authors nothing. Between Turns the Run stays blocked under the same owner (ADR 0031). end-interactive-step is a durable Operation admitted only at a Turn boundary; it settles the Attempt succeeded and advances the routing. There is no phrase, marker, or timeout that ends the Step.
  • Headless Preflight refuses a Bundle whose routing contains an interactive-agent Step with code interactive-step-needs-tui and remediation "run this Bundle in the TUI". The current "no headless execution in this release" refusal is deleted.
  • A send-interactive-turn in the TUI may not carry a per-Turn model choice in M3 (profile: unavailable).

Run Store schema

  • harness_session (session key, native session id nullable, availability, harness, profile digest, created/updated); turn (turn id, attempt id, session key, origin, sequence, admitted_at, result kind and detail nullable, settled_at nullable); turn_event (turn id, seq, kind, payload, at) for normalized durable events; transcript_entry (session key, seq, turn id, role, content, at); pending_gate (attempt id, step id, shape, message, output artifact name, raised_at). Attempts gain effective_model.
  • Admission ordering: turn row written before the stdin frame is sent (the durable recorder the Adapter awaits); a write failure proves not-started. Result and events append only; a settled result is immutable.
  • Ephemeral Harness Requests are not stored; their lifecycle appears in turn_event.

Projection Port

  • run Projection: durable view gains Session availability per named Session, current Turn position, effective model, pending gate of either shape, and new timeline entry kinds (turn-started, assistant-content, tool-activity, request-raised, request-answered, request-expired, turn-settled, interactive-turn-sent, interactive-step-ended). A new live overlay update kind carries Turn phase, outstanding requests with offered decisions, current activity, preview text, context and usage observations, and a live generation counter. ProjectionUpdate gains live and preview discriminants.
  • New Operations: answer-human-gate accepts a free-text answer; send-interactive-turn, end-interactive-step, answer-harness-request, steer-turn, interrupt-turn. Live controls settle accepted, rejected with a Problem, or indeterminate. Offers for live actions carry the live generation; a stale offer is rejected precisely.
  • New Resource References: Session transcript page (page form, opaque older cursor) and transcript export (export form). No native id or path crosses the Port.
  • Blocked status names basis and durability: durable Human Gate, interactive Turn, or ephemeral Harness Request.

Headless client

  • run launch and run resume gain --harness-requests allow|deny (default deny). While following a live Run the client answers each approval request through answer-harness-request by that policy; each answer is timeline-visible as "answered by client policy". No other client-side policy exists; the Harness still prompts through the bridge.
  • run answer gains --text <value> for free-text gates; --continue/--stop stay for approve-reject and checkpoints. Resting at a gate, exit codes, and --json shapes are unchanged; new JSON fields are additive.
  • run show renders the timeline entries above and the effective model.
  • Ctrl+C during a live Turn interrupts the Turn and rests the Run halted; the process then restores the default signal disposition and re-raises, so it exits with 128 plus the signal number (130 for SIGINT), never a fabricated 1. The halted rest is reconciled at the next open of the Workspace group (amended 2026-09-17, Audit: M3 row A16).

TUI Workbench

  • Timeline-first, per Prototype Crucible's launch and Run information architecture #23. Live Turn activity renders under the durable timeline with preview text replaced on settle. Approval requests replace the bottom input with an inline decision control (allow / deny, Enter confirms, Esc denies) and the request body shows the tool and input; the prompt input is disabled only while a request is outstanding. Free-text gates replace the bottom input with a text input. Interactive steps hand the bottom input to the human; End Step is a bound key that arms a confirming keypress and is offered only at a Turn boundary. Interrupt is a two-press key with a visible "again to interrupt" hint. Steer shows as unavailable with the profile's reason. Header shows Claude Code version and effective model.
  • Every visible control dispatches a current Action Offer through an Operation. The Workbench keeps using the injected RendererPort so the fake renderer drives it in tests.
  • Every TUI slice copies the Prototype Crucible's launch and Run information architecture #23 deferred items (keymap and focus, terminal layout, timeline mechanics, large content, interaction tuning, visual and accessibility verification, renderer and platform evidence) into its acceptance criteria.

Recording tool, sidecar, replayer

  • The harness test domain owns the recording tool. It is opt-in, runs against the installed Claude Code, and drives a named scenario through Secant's own Claude Code Adapter and bridge so what it captures is exactly what the Adapter will see. Per case it writes: the byte-faithful stdout stream, the stdin frames Secant sent with their ordering relative to stdout, the bridge calls and answers with their ordering, a per-Turn Workspace patch (git diff of the scenario Workspace before and after the Turn), and recording.json with the six required fields. redactions lists every substitution applied (home directory, user name, bridge token, any credential pattern); the tool refuses to write a recording that still matches a credential pattern.
  • The replayer is a script run by the Bun runtime present on the CI runner, dropped on a temporary PATH under the name claude (POSIX shebang; npm-style .cmd shim naming the runtime on Windows, so the shim resolver is exercised). It parses the argv Secant passes, checks the flags it depends on, waits for each stdin Turn before emitting that Turn's recorded stdout, applies the Turn's Workspace patch at the Turn's result, performs the recorded bridge call against the URL and token from the MCP config and blocks until answered, honours --resume by replaying the recorded resumed process, exits with the recorded code, and exits 143 on SIGTERM. It proves nothing about compatibility with the real Claude Code; the sidecar's provenance and the per-release human run make that claim.
  • Recorded cases M3 needs: the Test Repair scenario (baseline fail, one fix Turn with an Edit approval, converging test), interrupt mid-Turn, resume of a detached Session, authentication failure result, protocol-corruption line, and the Matt front's grill and spec Turns.

Gate and smoke

  • The deterministic suite runs the Test Repair Bundle through the headless client with --harness-requests allow against the replayer on the three-OS matrix, asserting succeeded, the approval recorded, the effective model recorded, and the commit made after the approve gate.
  • The package smoke, already running from the installed binary, grows one headless Proof Bundle Run against the same fake and asserts exit 0. The M2 smoke's assertion that the Proof Bundle is refused with "step-kind-not-executable" is deleted and replaced by a refusal assertion for an Interactive Bundle in headless.

Bundles

  • The Proof Bundle's manifest is unchanged; its fix Step is the first Agent step executed.
  • A second maintained External Bundle under the Bundles directory, the Matt front: interactive-agent grill (named Session) → human-gate approve-reject → agent writes the spec in the same Session, with maintainer-supplied skill assets referenced through uses and never edited. It is sequenced after the Proof Bundle scenario is green and is exercised in the TUI against the fake.

Dependencies

  • @modelcontextprotocol/sdk (Harness Module only; growth rule: protocol versions drift, clarifications may land later). which remains the sole PATH walk, now in process. No new Bun API; children spawn through node:child_process. No Agent SDK, no PTY, no shell.

Testing Decisions

  • A good test drives the highest seam with real collaborators and asserts observable outcomes: Run state, Attempt outcomes, timeline entries, Artifacts, exit codes, and the exact Problems returned. It never inspects Adapter internals, raw frames, or database rows directly.
  • Projection Port tests (primary). Wire the Application with the real Claude Code Adapter and the replayer on a temporary PATH under claude for the end-to-end scenarios (launch, follow, approve, gate, interrupt, resume, interactive Turns, End Step), and with the fake Adapter for orchestration paths the recordings cannot produce. Prior art: the M2 Application and run-projection tests that follow a live Run through the Port.
  • Deterministic fake Adapter (ADR 0022: "a production Adapter plus a deterministic fake"). The harness test domain owns an in-process fake implementation of the Harness Adapter Interface, scripted per test. It exists for two reasons the replayer cannot serve: it exercises Interface behaviours Claude Code never exhibits (structured clarifications, native steer, history replay on recovery, several concurrent requests, lost variants), and it lets Application and Port tests run without spawning processes. It is never the only end-to-end double (ADR 0027).
  • Conformance suite at the Harness Seam (ADR 0022 requirement): one shared suite run against both the fake Adapter and the Claude Code Adapter over the replayer, covering prepare and profile evidence, terminal ordering, concurrent requests, control rejections as values, interrupt and lost paths, recovery by resume, and idempotent close. Running both keeps the fake honest to the Interface.
  • Headless end to end on the three-OS matrix and the package smoke, as the acceptance path. Prior art: the M2 command-gate scenario and the package smoke.
  • TUI through the fake RendererPort with testRender, asserting the offer-gated controls dispatch Operations and that request, gate, interactive, and agent entries are distinguishable without colour. Prior art: the M2 Workbench gate-answer tests.
  • process Module unit tests move with the resolver; the fake .cmd shim test runs cross-OS via injected platform and on the Windows runner for real.
  • Architecture checks: module policy row for process, Harness-only fencing of the MCP SDK, the sidecar six-field assertion activated by the first case directory, guidance-structure limits for the Harness AGENTS.md.
  • Await observable readiness, never sleep. No CI retries. Named gaps: the fake proves nothing about the real Claude Code; the real run is M4's release checklist.

Out of Scope

  • Codex and Gemini Adapters, Harness selection UI, and pass condition 3 (M4, M5).
  • Release machinery, the real installed-Claude Code human run and its report (M4 release checklist).
  • Native skill delivery, Bundle-recommended models, model selection at launch or per Turn, /model mid-Session.
  • Structured clarifications for Claude Code (reported unavailable until a public contract exists).
  • Session-preserving mid-Turn interrupt through the SDK control protocol; a non-prompting permission mode; "always allow" decisions or persisted allow rules.
  • Headless stdin passthrough and terminal prompting for Interactive agent steps and requests (stays "not yet specified").
  • Agent step Artifact publication beyond the transcript; transcript export as an Artifact.
  • A settings file for Harness configuration beyond the one environment variable.
  • ADR 0021 uninstall, ADR 0025 forced removal, ADR 0029 built-in shipping (unchanged deferrals).
  • OpenCode vocabulary or code; TUI virtualization or mouse interaction.

Further Notes

  • Tensions resolved here: the spine's "delivered natively by the Harness Adapter" for the Matt Bundle's skills is superseded by ADR 0022's plain-path amendment; Define cross-platform test and release gates #26's "generic Node script" and npm-prefix smoke wording are restated under ADR 0030 (Bun runtime script, packaged-binary smoke). The M3: First Harness (Claude Code) #106 body's "Approve/reject landed in M2" is true only for derived Review checkpoints; authored Human Gates become executable in M3.
  • Claude Code facts checked against current documentation: --permission-prompt-tool waits for its MCP server up to the MCP startup timeout before the first Turn; the prompt tool returns allow with the input or deny with a message; --permission-prompts none exists but is not used (curation chose the bridge); a whitespace-only stream-json message ends the Turn without calling the model, so the Adapter rejects blank interactive input before sending.
  • OpenCode reference points used: await the stdin write's backpressure promise to keep NDJSON framing intact; use overlapped pipes on Windows; wait for close not exit; record an aborted Turn as a completed record carrying the abort, never a torn one; disable the prompt input only while a permission request is outstanding; two-press interrupt with a visible hint; mode-stack keymap so Esc means deny inside a request and interrupt outside it. OpenCode has no Claude Code CLI integration and no stdio recordings; its HTTP cassette recorder is not byte-faithful and cannot express timing or cancellation, which is why M3 owns its own recorder.
  • The process Module is the answer to M2's hand-over about the resolver's placement.
  • Headless answering of approval Harness Requests by a declared allow/deny policy is recorded as a 2026-09-16 amendment on #19; the 2026-09-07 amendment there covers only durable waits (Human Gates, Review checkpoints, interactive Turns), which rest, whereas Harness Requests expire with the Turn and cannot.

Amendments (2026-09-17, Audit: M3)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions