Skip to content

Phase 1: the run seam and the Claude Code reference runner (ADR-0008) - #43

Merged
JumpTechCode merged 3 commits into
mainfrom
phase1/run-seam
Jul 27, 2026
Merged

Phase 1: the run seam and the Claude Code reference runner (ADR-0008)#43
JumpTechCode merged 3 commits into
mainfrom
phase1/run-seam

Conversation

@JumpTechCode

Copy link
Copy Markdown
Collaborator

The second adapter seam. port.Run administers a sitting; the Claude Code reference adapter implements it against print mode; a shared conformance kit checks the contract half. Nothing consumes it yet — the Invigilator is deliberately not designed here.

What the binary said that the documentation did not

Three real invocations, ~$0.020 total, and each one changed the design:

Probe Finding Consequence
--max-turns 1 on a task needing a tool subtype: error_max_turns, exit 1 The flag works despite being absent from claude --help. An earlier draft declared turn caps unenforceable on that omission and would have refused a cap it could keep.
--max-budget-usd 0.001 stopped, and cost $0.0047 A cap is a threshold noticed after it is crossed, not a ceiling. EnforcementNative says so explicitly.
both both exited 1, as a crash does The exit code cannot be the field that decides. The harness's own subtype is.

The turn cap also reported num_turns: 2 against a cap of 1, and a budget-stopped run came back with its token counts zeroed while the cost was real. Both are pinned as tests rather than fixed.

The shape

  • assay.Assignment / Sitting / Guarantees / Caps — domain types, since the Examiner, Ledger and Reporter all consume a sitting.
  • assay.StopReason with Decides() true for exactly one value. ERROR ≠ FAIL moved out of a reviewer's memory and into the type.
  • port.Refuse — shared, so "never silently drop a cap" is not an invariant every implementer re-derives. An undeclared cap or field is refused before a process starts, so a refusal costs nothing.
  • port.Query.Native — closes the loop from a sitting back to a Session without either adapter importing the other, leaving ADR-0005 untouched. Matches on the transcript's file name: 400 of 400 sampled transcripts are named for the session they hold, and matching on contents would mean opening every file in a store that held 2,485. Fails closed.
  • --bare plus an empty --setting-sources is the hermeticity keystone. --fallback-model is never passed, asserted as an absence.

The conformance kit's honest limit

VerifyRun never calls Sit on a path that can reach a harness. Reading a transcript is free; administering a sitting starts a coding agent against a real model on somebody's key, and a kit that spends a contributor's money to prove an adapter well behaved is a kit that gets deleted from the test run. It checks declarations and refusals only, and says so in its own doc comment — the same way Verify already admits it checks the contract half and not whether an adapter parses anything.

Whether the adapter drives the harness correctly is proved in its own tests: three verbatim recorded results under testdata, plus shell stubs for the wall cap, cancellation, a missing binary, unreadable output, and stderr bounding.

A ReferenceRun joins the fictional capture adapter — a command-template runner that guarantees almost nothing, so the kit's refusal checks have something to check. The measured adapter guarantees everything, which would have made them vacuous.

Verification

make verify green: build, vet, gofmt, race, coverage 89.7% (threshold 80), golangci-lint 0 issues, govulncheck 0.

Known open edge

The guarantee check runs one way, like the capability check it mirrors: over-reach is caught, a guarantee declared and never kept is not. Same open edge as the capture side, tracked as #39.

JumpTechCode and others added 2 commits July 26, 2026 22:45
The second seam. port.Run drives a harness through work it has already
done once; the Claude Code reference adapter implements it against print
mode, and a shared conformance kit checks the contract half.

The guarantees were measured rather than read. Three probes against the
real binary settled what the documentation could not:

  - --max-turns exists and works, though `claude --help` does not list
    it. An earlier draft declared turn caps unenforceable on the strength
    of that omission and would have refused a cap it could keep.
  - Neither cap bounds anything. A run capped at $0.001 cost $0.0047; a
    run capped at one turn reported two. A cap is a threshold noticed
    after it is crossed.
  - Both cap hits exited 1, the same status as a crash. The exit code
    cannot be the field that decides, so the harness's own subtype is.

That last one is why StopReason exists and why exactly one of its values
returns true from Decides(). ERROR is not FAIL, moved out of a reviewer's
memory and into the type.

Guarantees are declared per cap and per assignment field, and asking for
something undeclared is refused before a process starts by a shared
port.Refuse — a dropped cap would produce a sitting that looked clean and
a budget nobody kept. The check runs one way, like the capability check
it mirrors.

Query.Native closes the loop from a sitting back to a Session without
either adapter importing the other. It matches on the transcript's file
name: across 400 sampled transcripts the name was the session identifier
every time, and matching on contents would mean opening every file in a
store that held 2,485.

The conformance kit deliberately stops at the process boundary. Reading a
transcript is free; administering a sitting starts a coding agent against
a real model on somebody's key, and a kit that spends a contributor's
money to prove good behaviour is a kit that gets deleted. It checks
declarations and refusals; the fixtures under testdata are verbatim
results from the three real runs.

ADR-0008 records the decision. The Invigilator is not designed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
Three independent reviews, one lens each: runtime correctness, contract
and invariants, tests and claims. Two blockers, both proven rather than
argued, and both in the gap between what this seam declares and what it
does.

The wall cap did not hold. exec.CommandContext kills the child it
started, and the wait then reads the output pipes to end of file, which
never arrives while a grandchild still holds them. Against a 150ms cap a
forking stub left Sit blocked past 20 seconds. Worse, the previous test
comment named this mechanism and wrote around it with `exec sleep 30`, so
EnforcementSupervised was a promise whose one failure mode was the case
the test avoided. cmd.WaitDelay fixes it and the new test fails without
it.

The environment was an open configuration channel. cmd.Env was never set,
so the harness inherited everything — including ANTHROPIC_MODEL,
ANTHROPIC_BASE_URL and CLAUDE_CODE_USE_BEDROCK, all of which it reads. A
developer with ANTHROPIC_MODEL exported would have got sittings run
against a model no report names, on a tool whose purpose is noticing when
the model changed. Now an allowlist, with the two consequences stated:
env-only Bedrock and interactive sign-in both fail loudly instead of
quietly measuring something else.

Also from the reviews:

  - Assignment.Dir was governed by nothing. An empty value meant the
    harness ran in Assayer's own working directory, which is the user's
    repository. Refuse rejects it, and a new default-deny test over
    Assignment's fields makes the next ungoverned field fail rather than
    ship.
  - A negative cap was silently dropped by every `> 0` guard — the one
    direction that costs money, and reachable from an orchestrator
    computing a wall cap as time-until-deadline.
  - Sitting.Usage.Wall meant two different things. A sitting measures
    process duration; Usage.Wall means a transcript's timestamp span,
    which for an interactive golden session includes hours of idle.
    Sitting.Wall is now its own field. Sitting.Turns joins it, so a
    report can say "stopped at 2 turns of 1" without resolving a session
    that may not resolve.
  - The token test could not fail: success.json reads nothing from cache,
    so the sum was indistinguishable from one ignoring the field that
    carries 96.5% of input. Asserted against turns.json instead.
  - Query.Native was bound by nothing — no kit check, and the reference
    adapter ignored it, so any adapter could too.
  - The stderr bound is documented as keeping the tail; keeping the head
    passed. The stubs broke on any checkout path containing a space.
  - Refuse panicked on a nil assignment, on an interface taking a
    pointer.
  - "Never calls Sit on a path that can reach a harness" was not true of
    the kit, and "both cap hits exited 1" was asserted in five places
    with no record in the repo. The first is reworded, the second is
    written down in testdata/README.md beside the fixtures it describes.

ADR-0008 gains the couplings it should have declared: tool names and the
settings blob are harness vocabulary riding through the leaf, and a token
cap is missing from Caps. Seven remaining findings are filed as #44#50.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.91275% with 39 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/adapter/conformance/run.go 65.57% 15 Missing and 6 partials ⚠️
internal/adapter/conformance/reference.go 47.36% 10 Missing ⚠️
internal/adapter/conformance/verify.go 25.00% 4 Missing and 2 partials ⚠️
internal/assay/enum.go 96.61% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

The only untested statement left in the run adapter was the line choosing
what to execute when no binary is named — which is the path a composition
root takes, so a typo in the harness's name would have shipped. The stub
goes on PATH rather than being named directly, because the lookup is the
thing under test.

Writing it turned up the environment allowlist working: substituting PATH
outright left the stub unable to find cat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ew64uotJqtvVDN3twMAVEQ
@JumpTechCode
JumpTechCode merged commit 0bac725 into main Jul 27, 2026
6 checks passed
@JumpTechCode
JumpTechCode deleted the phase1/run-seam branch July 27, 2026 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant