Skip to content

ADR-0066 Architecture decision record

A @model tier runs a real model, and a change to what the model reads runs the tier ​

Every tier fakes the model. The default e2e tier holds a stub Harness; conformance and @kind run the stock Harness against a scripted provider that chooses each Turn's shape (ADR-0027, ADR-0038); @dist runs ping. So no suite can see a change in what a model does with what jr2 shows it. The evidence that such changes are large sits in prose: ADR-0029 and ADR-0062 measured the Allowed picks by hand on the home-lab vLLM model — 0/10 obeyed ahead of the prompt, 10/10 after it — and nothing re-runs that measurement when briefing.ts, the Menu's rendering, the Working tools' descriptions, Compaction, or the pi pin changes. briefing.test.ts pins the order of the text; it cannot tell whether a model still obeys it. And the paths a real Agent takes through the kit as a whole — read a Repo, edit in a Worktree, run the tests, commit, hand off, be refused, recover, finish — are exercised only one scripted tool call at a time.

Decision ​

  • An opt-in @model tier: the @kind tier with the last fake removed. Same shared vanilla kind cluster, same bring-up (just e2e-kind-up), same jr2 up per scenario into a fresh namespace, same seed Repo, same Custodian path — the endpoint's key is a Held secret bound to its host, and the Harness container holds a Stand-in (ADR-0059). What changes is the provider the Custodian swaps toward: a real model endpoint instead of the World's scripted one. The pod runs the stock Harness, the model originates every tool call, and the wire, the container boundary, the git transport and the Machine are all the product's own. Tagged @model, profile model in cucumber.json, just e2e-model, excluded from the default profile and from @kind.
  • Two kinds of scenario: claims and the run. A claim scenario re-states one ADR's measured claim as trials with a floor: a Turn obeys its Allowed picks against a prompt that argues for another (ADR-0029, ADR-0062); an Agent works where its Frame says (ADR-0057); a Menu-only Agent picks without reaching for tools it has not got (ADR-0031); a notice changes what the next Turn does (ADR-0061, ADR-0062); a Turn follows its modes — after a pick changes the mode, after a correction receipt, and after a model that stopped is asked again (ADR-0015, ADR-0029, ADR-0016); a model that stopped with nothing allowed is woken when a Gate's answer opens a pick, and picks (the wake, ADR-0016); a Turn hears its Allowed picks change under it at a step boundary and follows them without a refused pick (ADR-0029); and a pick that runs work is answered at Rest and the Agent acts on the answer, after a chain of replies and after a Harness restart cut the pick (ADR-0024, ADR-0031); and after a signed push the Agent continues on its branch, so its next push publishes (ADR-0067). A claim qualifies when an ADR states what a correct Agent does differently for it and a step can observe the difference from outside the pod. The run is one workspace() workflow through to done on a real task: a ticket the seed Repo carries as a failing test, a planner's pick, a coder that edits in the Worktree, runs the test, commits on the run's branch and names the commit — a guard refuses a submit that names none — and a reviewer in the same Sandbox, read-only (ADR-0028), that runs the test itself and approves or rejects, bounded. Approval parks at a Gate, and the witness is the pod (parking is retention, ADR-0012): the branch is ahead of main, its head is the named commit, the tree is clean, and the test passes when the suite runs it there. Nothing is pushed — the Agent holds no credential and the push is the caller's (ADR-0005, ADR-0053), as task says — and the seed's app.git accepts none. The run asserts the pick ledger's shape and a time bound — never the diff, the commit message, or the Turn count, which are the model's.
  • Scored, not asserted. A claim is N trials per cell (thinking off and on are cells, set through the Dial), and a cell fails only below the floor written in its scenario. The floors are the ADRs' numbers rounded down to what a model at the floor can hold: 9 of 10 where the ADR measured 10/10. Every run writes a scorecard, features/.tmp/model-scorecard.json, keyed by model id and cell; a committed baseline per model id sits in features/model-baselines/, and a cell that fell below its baseline while still above its floor is reported, not failed. Drift is a signal to read; a floor is a defect. Each cell also records what its trials spent: the tokens and the cost summed off each trial's Settlements (ADR-0027), in the scorecard and as one line of features/.tmp/model-usage.jsonl, and the tier prints the cell's cost beside the cost of the month so far. A trial whose usage cannot be read records that, and never fails.
  • The instrument is a model shown to catch the known regressions. Today that is the home-lab vLLM model (Qwen3.6-35B-A3B, FP8 weights). ADR-0029's numbers are why a strong model does not qualify by default: a frontier model obeys either placement and hides the regression. A model qualifies when the known regressions, put back, take their cells below the floor: (a) ADR-0029's, the Allowed picks stated ahead of the prompt (obedience; Qwen obeyed 0 of 10), and (b) a draft's Chart briefing text that Qwen admitted 5 of 10 times (chart-refusal, floor 7; a snapshot kept outside the repo). On 2026-10-25 Claude Haiku 5.5 caught neither: with the picks ahead of the prompt its first call was the allowed hold in 20 of 20 trials, and on the draft's text it admitted 10 of 10. So a Haiku run checks the tier's machinery, cheaply, and never a change to what the model reads. The same model served another way is another instrument: NVIDIA's NVFP4 checkpoint on vLLM 0.32 with MTP, about twice the throughput, took answered from 10 of 10 to 6 of 10 (4 trials fixed the failing test and shipped) and chart-refusal to 4 of 10, so the home lab serves the FP8 weights on the image the baselines were measured on. Any other model must clear the same floors and cannot lower them; adding a model adds a baseline row, never a floor. The run is measured once per invocation, on whichever endpoint is selected, because it costs minutes and its failure is a defect to read, not a rate.
  • The endpoint is a profile, selected by env, and the key never enters a file. features/model-endpoints/<name>.json carries id, api, baseUrl and model, plus the tier's own thinking cells, trials, parallel and workers (below), and optionally a caBundle path for a private CA (ADR-0020). A profile is one of two kinds, as a user's instance is. A profile with key names one of pi's catalog providers (anthropic), and the catalog holds the model's limits and thinking levels. Its key is a Held secret in secrets, under the known model key that key names (ANTHROPIC_API_KEY, ADR-0059). A profile with no key is a harness.provider, and carries its models' windows. JR2_MODEL_ENDPOINT=<name> selects it; JR2_MODEL_API_KEY is read from env alone. Before the converge, the fixture copies the selected profile to a fixed path inside features/model-instance/. The instance's config and Agents import it from there, so the in-cluster Orchestrator reads the same values the host did. An env read in a definition is undefined in the pod, as the @kind config notes. That path holds one profile, so a lock on it (features/lib/model-lock.ts) records the profile, the suite and the pid that owns the suite. The workers of one suite share it. A second suite in the same checkout fails at once. With another profile, the refusal names that profile, because the suite would converge against it and file its cells under its own key with no error: two profiles at once need two worktrees. With the same profile, it fails too: the lock names one owner, so a later suite with another profile could take it over while the second still converges, and two suites put twice workers × parallel runs in flight. A lock whose owner is gone is taken over. Nothing in the product learns the tier exists. Reachability from the pods is jr2 up's provider preflight, for both kinds of profile. It checks each model from a check pod with a Custodian before it writes anything (ADR-0019). So a pod with no egress to the provider fails the converge at the check pod.
  • Not in CI. Run by whoever changed what the model reads, before calling the change done. Not in pnpm -r test, not in the release job: it needs a cluster and an endpoint, it is nondeterministic by nature, and a gate that flakes is a gate everyone re-runs. just e2e-model probes the profile's endpoint first and, when nothing answers, exits 0 with one line — skipped: no model endpoint at <url> (profile <name>) — the @model tier did not run — so it can always be invoked and a skip is never silent. It skips with one line in two more cases. A profile with key and no JR2_MODEL_API_KEY prints skipped: JR2_MODEL_API_KEY is not set, and profile <name> needs it (<key>) — the @model tier did not run, where <key> is the profile's key. An endpoint that answers with a status other than 2xx prints skipped: the model endpoint at <url> answered HTTP <code> (profile <name>) — the @model tier did not run. The trigger is a change to any text or tool the model reads or is offered: the Briefing, the Menu's rendering and the refusal, the nudge, the Working tools and their descriptions, Compaction's summary, the admit body's notices, the pi pin, a provider adapter, or an authoring surface that changes a Frame. An agent reviewing a feature or refactor that touched one of these runs the tier and reports the scorecard's floors and drift, or the skip, in its handoff. CLAUDE.md states the rule. One scenario runs alone by its title, which is the scenario filter: cucumber-js --profile model --name "a Gate's answer during a command is followed".
  • A third pi canary. Conformance and @kind drive the turn loop against a script; this tier drives it against a model that can misread. Run all three before bumping the pin.

Considered options ​

  • A real model inside @kind. Rejected: @kind proves the data plane, and its scenarios must be readable when they fail. A model that misreads a prompt would make a Sandbox failure look like a wording one, and a laptop without an endpoint could no longer run the data-plane tier at all.
  • In-process, in @jr2/harness: conformance with a real baseUrl. Rejected: it measures the Harness's rendering of an admit body the test wrote, not what the Orchestrator sends — the Allowed picks, the Frame and the notices are composed there — and it has no Workspace, so it can never run the run.
  • Pass or fail per trial. Rejected: a model is a distribution. A single 9/10 fails a green change once a week and teaches everyone to re-run until it passes, which is the same as not measuring.
  • A frontier model as the instrument. Rejected: ADR-0029 measured the regression only on the weak model; the instrument must be the one that can fail.
  • Recorded provider transcripts, replayed. Rejected: a replay is a scripted provider by another name, and the tier exists precisely because a scripted provider cannot misread.
  • A hosted model in CI. Rejected for now: a key in CI, a bill per push, and nondeterminism in the gate. The decision is who runs it, and that is the change's author, when the endpoint is there.

Consequences ​

  • ADR-0010's tier list gains @model; ADR-0027's canary sentence names three canaries; CLAUDE.md's testing strategy gains the tier and the rule about when to run it.

  • New: features/model.feature, features/steps/model.steps.ts, features/model-instance/ (config, _agents.ts, the claim workflows and ticket.ts), features/model-endpoints/homelab.json, features/model-baselines/, the model and model-warm profiles in cucumber.json, features/model-warm.feature, features/lib/model-lock.ts, and just e2e-model and just e2e-model-warm.

  • The seed Repo's main gains a small project with one failing test, so the run has a task with a checkable outcome. @kind reads the seed's refs, never its files, so it is indifferent.

  • ADR-0029's and ADR-0062's measurements become scenarios; their prose keeps the numbers as the origin of the floors.

  • The Menu-only claims read the model's calls off the Instance Harness's printed conversation (ADR-0023) — the one place a refused pick, a nudge and a restated Turn part are all visible. Every line of a conversation carries its Instance ID, which starts with the run id (ADR-0023), so a trial reads only its own run's lines of that one log, and the Menu-only trials run parallel at once, as the Sandbox claims, which read their pods, do. Two claims keep fewer in flight. cut-pick runs one trial at a time: each of its trials deletes the Instance Harness pod, which would cut the Turn of every other trial in flight there. noticed runs at most two: on a loaded host the kernel's OOM killer takes the container before the guard takes the hog, and that trial is void. A claim whose setup the step must race (a Gate answered while the model works, a mode moved while it composes) voids the trials whose setup lost the race; voids are bounded, and a void past the bound counts against the floor and prints as a void in the failure list, never as a miss.

  • Scenarios run workers at once, set per profile. Each scenario has its own namespace, so scenarios are independent, as @kind's are. The profile's workers is how many run at once: just e2e-model passes it to cucumber as --parallel when it is above 1. It is in the profile and not in cucumber.json, because its limits are the endpoint's and the host's, not the tier's. workers × parallel runs are in flight, and that product must stay within what the endpoint serves (the home-lab vLLM serves 16 streams; a run past them waits in its queue and skews every trial's latency and the runaway bound) and within what the host holds: about 0.85 CPU and 2.2 GiB per scenario, plus 0.5 CPU and 1 GiB per Sandbox trial in flight, on a 16 CPU, 45 GB host that three kind clusters share. The home-lab profile runs 4 × 4: a full run took 26.5 minutes on 2026-10-25, against about 70 one scenario at a time; Haiku's runs 4 × 2, 10.6 minutes. Cucumber starts scenarios in file order, so model.feature lists its longest Rules first. chart-refusal is the longest, 21 of the 26.5 minutes: its trials write long answers and call run_chart up to 16 times. A step reads a run's status from the Orchestrator API that jr2 status uses, with the same Instance token, over one port-forward per scenario, opened on the first read and again when it dies; a jr2 status process per read every 2 s cost about 0.4 host cores per trial in flight. On a cold host, just e2e-model-warm runs one converge first and serially (features/model-warm.feature, tagged @warm and excluded from the model profile), as e2e-kind-warm does for @kind, so the workers do not each build the same images. Converges on two clusters at the same moment share the host daemon, and one's sweep keeps only its own cluster's images (ADR-0039), so it can take the other's fresh Sandbox image before its use. jr2 up builds a ref it finds missing again from BuildKit's cache and goes on; before it did, 5 of 19 converges in one workers: 4 run failed this way.

  • Each trial's line in the failure list says how its run ended, whatever stopped the trial (a driver that threw prints what it threw, and names the run it had started, if any: threw: <message> (run <id>)). A run that ended failed also prints its Failure, and a run in error its fault, which carry the reason (features/lib/model-status.ts): the namespace that held the pod log is gone when the list is read.

  • Every trial keeps its evidence. Every trial, passed, missed or void, keeps one folder, features/.tmp/model-trials/<model>/<cell>/<runId>/ (gitignored with .tmp; a cell's / is a folder; a byte of a name outside [A-Za-z0-9._-], such as a model id's :, and a leading . are spelled %XX, so any model id keeps its trials):

    • conversation.txt: the Harness container's printed conversation (ADR-0023), timestamped. A Sandbox claim's is followed from the moment the trial's Sandbox pod exists until the reap at settle takes the pod (ADR-0012), so a read after the settle is never needed, and it is followed again after the container restarts. A Menu-only claim's is its own run's lines of the Instance Harness's log from the trial's start, kept by the Instance ID's prefix <runId>/, and followed again after the step deletes that pod and the StatefulSet makes it again. A driver reads its measurement from this kept conversation, never from a second read of the log: kubectl logs reads only the current container, so a restart or a 10 Mi rotation loses lines that the follow kept. It holds each pick's answer after its call, the text the model read ([<pick> answer] <text>, or [<pick> error] <text>): the receipt, a command's outcome, a refusal, the paragraph after a signed push. An answer longer than the print's bound (2000 characters, a call input's) is cut in its middle, with the number of characters dropped marked at the cut (… [<n> characters cut] …), so the receipt at its head and the end with "Allowed now" both stay; the model read the whole answer, and the cut is the log's copy only (ADR-0023). The Harness prints it (ADR-0023), so the tier keeps it with the conversation, and nothing the model reads changed to keep it. answersIn reads each call with its answer; a call a Harness restart cut has jr2's text, which the next container prints before its first prompt, and a call an abort cut while the model was still writing it has jr2's text that it did not reach the workflow, printed before the conversation's next prompt.
    • effector.txt: the Sandbox Effector container's log, followed the same way, for a Sandbox claim; it says so when the pod has none.
    • harness-history.txt: the Harness history view (ADR-0027) of each conversation whose prompt printed in the kept conversation, at the Instance ID its lines carry: each settlement, with the failure text of one that failed. The view is read while the trial runs and again each time a settlement lands, and the last one read is kept. A Sandbox Harness exits at the reap, so the view after a Turn that settled just before it can be too late to read: each Agent's stream is also followed, read as the Orchestrator reads it, and each submission-settled it carries that the kept view does not hold is kept after the view.
    • record.json: the Machine's final record, the last status the driver read of the run.
    • push-log.txt, for a push claim: the push remote's log from main to each commit the record says was published.
    • instance/, for a Sandbox trial whose Workspace encloses a Menu-only seat (ticket's planner): that seat's conversation.txt and harness-history.txt, read off the Instance Harness where its Turns run (ADR-0031), so that its Settlements are in the trial's usage.

    The run keeps its evidence the same way, in its cell's folder, from its start until its cell is written or its scenario ends.

    Each file is written when it changes, so a trial that hangs until its timeout leaves what it had. Every probe only reads: a probe that fails writes <unavailable: why> into its file and never changes what the next trial does. A Menu-only driver measures the kept conversation, so when that follow fails, the trial has nothing to measure and is not measured, as before when its kubectl logs read failed. No other probe fails the trial. Each trial's note opens with what the model called, read off the kept conversation in the Menu-only notes' form (said bash (restated) ask_approval submit) from the lines of the Agents whose prompt printed there (the run narrative echo, [run] [status] and [run] [emit], is not a call), or said <unread: why> when it cannot be read, and the driver's note follows. A floor failure, a void past the bound and a cell below its baseline are read from these folders before a prompt, a floor or a step is touched. A failed @model scenario also writes the @kind failure dump, to the same place as a @kind one: features/.tmp/kind-failures/<namespace>-<scenario>/.

  • One trial may measure two claims: push-follow and push-no-refusal are two cells of one scenario's trials. After the measured pick, a coder that picked revise is back in coding, where only the desk's answer moves it, so the step answers the desk again each time, until the trial's time ends, and the trial can finish; the measure is still the first pick after the first answer. Until 2026-10-23 the step answered at most three more times (below, 5d3096da). The driver changed for this after the claim ran once (70f9cb2); the user approved the change on 2026-10-09. Those answers never reached the Machine: the driver waited for the run's own value to read coding, and a workspace() run's value is its wrapper's, which never holds its body's state, so the step never answered again. In the full run of 2026-10-12 one trial (ba79f322) recorded its measure, revise right after the answer, then looped ask_approval and run_tests in coding, reading "Allowed now: ask_approval, run_tests", until the run faulted, and failed "every trial finished". The driver now reads the body's state off the children jr2 status reports (features/lib/model-status.ts), a defect fixed and no setup change. Then a bound of the kind of the finished rule below: a trial whose measure is recorded — its first pick after the answer; the body seen back in coding after it is revise — and that does not finish within its time is cancelled and counts as finished. Its time is a trial's bound, 15 minutes, counted from the desk's first answer. A measured trial whose run settles before then without its task's final — a fault — is not cut, and fails "every trial finished". The prompt, the floor and the measure did not change. Both were decided under the user's deferral on 2026-10-12 and confirmed by the user on 2026-10-12.

  • The held-answer claims of ADR-0024 and ADR-0031 — chain-answer (the next pick follows the answer's last link) and cut-pick (after a Harness restart cut the pick, the Agent uses the answer and does not pick it again; the step deletes the Instance Harness pod). Each floor carries ADR-0029/0062's measurement of a list read last. The first run (home-lab vLLM, 2026-10-07) confirmed both, 10 of 10, now baselines.

  • The refusal of a second pick while one is outstanding is not a claim. It is jr2's mechanism, not something a model does with what it reads. The Orchestrator refuses it from the Machine (ADR-0024). The Harness runs only the first pick of one model answer (ADR-0024, ADR-0027). Harness conformance proves it with a scripted provider that sends two picks in one message. Only the first is delivered, and the second reads that it did not run. A claim for it (refused-outstanding: the refused pick is picked again once the answer comes) ran once, on 2026-10-07: in all 15 trials of that run the model called book_room, read its answer, then called send_invite, so no call was ever refused and its setup never happened. It left the tier with no cell; no floor was lowered. A model that sends two picks in one model answer would need a claim of its own: what it does after its second pick did not run.

  • The command-Effect claims of ADR-0067 — answered (the next pick follows a command's output), allowed-after-route (the next call is one the route allows, not one allowed before the pick) and unknown-outcome (after a Harness restart killed the command, the Agent checks before it announces; the step restarts the Sandbox's Harness container) — carry the borrowed floor of ADR-0029/0062's measurement of a list read last. Their first runs (home-lab vLLM, 2026-10-07 and 08, four runs of the tier) read:

    • answered: 10, 9, 10 and 10 of 10 with its first setup, now a baseline; 9 of 10 in the full run of 2026-10-12, and 8 of 10 in the full run of 2026-10-13 (on 50fe7e0), below its floor of 9. The two misses (53cc7436, 253e840b) had one shape: the coder read the right answer, the outcome exited with code 1, the output's line auth.test.ts: expected 200, got 500 and "Allowed now: report_failure, run_tests, ship", said that the output named auth.test.ts, a file the Worktree did not hold, ran the Repo's own tests, fixed the greet tests the output did not name, and picked ship. jr2 acted correctly in all ten trials. The setup was the one allowed-after-route had until 2026-10-08: the test command was fixed data that printed a failure of a file the Repo did not hold, while the Worktree (the seed's app.git) held real failing tests the model could fix, so a miss could not tell "ignores what the output says" from "fixed what it was shown". The setup now is the same real case (2026-10-13): the claim binds billing.git, and run_tests runs its suite in the Worktree (node --test), whose one failing test the output names with its file and the values it compared, and the Worktree holds nothing else to fix. The prompt's subject follows the seed ("The billing change is in." in place of "The auth fix is in."); the rest of the prompt, the floor, the measure (the first pick after run_tests is report_failure) and the baseline are unchanged. The user approved this setup change on 2026-10-13, before it was made. The first run with this setup (2026-10-13, on 1380dee) read 9 of 10, at its floor of 9 and below its baseline of 10, with no void: each of the ten answers named the real failing test (billing.test.js:9, 2148 !== 2149). The one miss (e3a27f22) read the failure, fixed billing.js, ran the tests again and picked ship, against the prompt's "if they fail, report the failure": the model's choice. It is reported below baseline, not failed; the claim is no longer open, and the baseline stays at 10.
    • unknown-outcome: 10 of 10 in both runs that read it whole, now a baseline. The Harness container restarts in place, and the reap when the run settles deletes the pod with every log it held, so the step follows the restarted container's log from the moment it runs. Read after the settle, the log was gone in 5 of 10 trials of the first run, each a model that had checked.
    • allowed-after-route: 3, 5 (a diagnostic run), 4 and 4 of 10 with its first setup, below its floor. Every miss had one shape: after the failing answer the coder read the seed Repo, found a failing test, fixed it with its Working tools, saw it pass, and picked ship, which the newest "Allowed now" it read (open_issue, run_tests) did not list. The setup made that failure say little: the test command was fixed data that always failed and named a file the Repo did not hold, while the Worktree held an unrelated test the model could fix. So a miss could not tell "ignores the newest list" from "fixed what it was shown". The setup now is the real case (2026-10-08): the claim binds its own seed Repo (billing.git), whose suite has one failing test that names the defect, and run_tests runs that suite in the Worktree. A correct Agent may fix the code, but only run_tests passing takes the workflow back to where ship is allowed; the miss is a ship after its own fix without the workflow's check. The floor and the measure are unchanged. Its first run with this setup (2026-10-08) read 10 of 10, now a baseline. The setup change (86e9691) was made before the user saw it; the user approved it on 2026-10-09.
  • continue-after-signed-push (ADR-0067): the coder fixes the seed's failing test, commits and publishes — a signed push from the Sandbox Effector over ssh to the seed's push remote, push.git — then commits a second change, publishes again, and finishes. The first push's answer carries the paragraph after a signed push. The measure is the Machine's record of the second push: pushed; an Agent that rewrites the pushed commits after their ids changed makes it diverged. A trial whose first push did not publish a signed copy is void, and so is a trial whose only push already carried two or more commits from main (below). Its floor is push-follow's, 9 of 10: the answer is the newest thing the model read. Its first run (home-lab vLLM, 2026-10-11) read 9 of 10, none void, at its floor: now a baseline (4316bdb). For it the tier's shared instance gained a push credentials entry (features/model-instance/jr2.config.ts) and its Effector image a passwd entry for uid 1000 (9cd3935). The user approved this on 2026-10-11 as not a setup change: the rule guards what the model is put in front of, and a credential entry and a passwd line change nothing the model reads. No existing claim's prompt, workflow or floor changed, and every existing cell held its baseline after the change. Its next run (2026-10-11) read 8 of 10, below its floor and its baseline, and a diagnostic run read 6 of 10. Each of the four misses whose log was read had one shape: the model made both commits before its first publish, published once, and finished. The claim measures the paragraph after a signed push, which the model reads only after a push; in such a trial the model chose its order before it read any post-push text and had no work left to continue, so the measured moment never occurred — the shape of the noticed and push-follow voids. The setup changed for it (decided under the user's deferral on 2026-10-11, confirmed by the user on 2026-10-12): when the setup holds and there is one push, the step counts the commits from main to that push's published commit on push.git, and two or more make the trial void ("void: the first push carried <n> commits …"). It decides from the push's content, never from the push count, so "published once, committed, finished" and "published, reset, finished" stay misses. The prompt, the floor and the pass did not change, and the 8 of 10 stands: the claim passes only on a new run. The void shares the claim's one bound of replacements (half the trials, rounded up) with the setup void, and a void past it counts against the floor and prints as a void. At the diagnostic run's rate, 4 of 10, the bound is often spent; the claim is then open, not passed. Rejected: counting the batched trial a miss (it measures the model's reading of the claim's own prompt), raising the bound, or lowering the floor.

  • A trial that reaches either of its task's finals finished. A claim whose workflow has two task finals (wake, chain-answer, and the three command-Effect claims) counts a trial finished at either; its measure already counts the wrong one a miss, and counting it unfinished too would fail the scenario on one miss — the floor 10 of 10 in all but name. A run that ended failed (a claim workflow fails on a fault and on a lost Workspace), a run in error and an unsettled run never finished.

  • wake (below, under push-wake) has run four times, 10 of 10 each, and is a baseline.

  • A claim's setup may change; its floor and what counts as a pass may not. A setup is the part of a trial that brings the model to the moment the claim measures. When a claim falls below its floor because that moment seldom happens, its workflow and its driver may change for the setup alone, and the reason is recorded here. A setup that no honest change makes reliable leaves the claim open, and it is reported as open. The first run of the mode and step-boundary claims (home-lab vLLM, 2026-10-05) changed two:

    • mode-stop. Its Turn asked for a review and told the model to wait; the stop happened in about 1 trial of 3 — models picked straight through (ask_review submit) or looped ask_review/revise — and the claim fell to 5 of 10 twice. ask_review now enters a mode whose revise and submit are closed, where the Briefing tells a model to stop, and a timer then opens them, so the wake asks it with the nudge's text (ADR-0016). The timer has no target: it assigns, and the picks' guards read it. A timer that moved the workflow to a mode inside the Agent's state holds Rest (ADR-0024), so ask_review would stay outstanding until it fired and its answer would carry the Allowed picks on the same call; no model would stop. The prompt is unchanged: it is the claim's fixed data. What counts is unchanged: the trial is measured at the first ask after the model's ask_review, and void without one, and it passes when the first Menu call after that ask is a pick the mode active at the ask allows. The driver reads the modes from the workflow, not from a copy of them, and replays the model's calls through them: a mode with a targetless timer has its picks closed from each entry until the timer fires; the driver takes the timer where a restated Turn part shows that it fired, and follows it at the ask when the wake asked with nothing allowed.

      On 2026-10-23 the setup changed again. In that day's full runs on Claude Sonnet 5.5 and Haiku 5.5 (below), the timer was the whole review: no reviewer answer came, and a careful model would not submit notes that no reviewer had answered. Now a reviewer, a child Machine below reviewing, reads the notes for 2 s and approves them. ask_review is outstanding while it reads, and its answer is the reviewer's output (ADR-0024), with the Allowed picks read at Rest: none, because the timer has not fired. So the model reads the answer that the prompt tells it to wait for, and it still stops with nothing allowed. The timer still has no target and starts at reviewing's entry, so the reviewer holds Rest only for its 2 s, and the wake still asks the stopped model. The states that the reviewer's hold adds (reviewing.reading, reviewing.answered) are below reviewing, so the modes that the driver reads do not change. The prompt, the floor and what counts are unchanged. The one new text that the model reads is ask_review's answer: The workflow took it and is back in the state that asked for this turn. Its answer: the output of `reviewer`: {"verdict":"approved","comments":"Clear and accurate."}., in place of the plain receipt's "The workflow is still in the state that asked for this turn." The user approved this change on 2026-10-23 (question 21: give the review a real answer). In the claim-setups run (below) it gave 10 of 10 on the home lab, on Haiku 5.5 and on Sonnet 5.5, and every trial finished.

    • push-wake, since replaced by wake. It measured a Menu-only Turn that started with nothing allowed and picked inside the same Submission once a Gate's answer was restated at a step boundary. The step first answered the board as soon as the Turn's prompt printed; in two runs, 8 and 9 trials had no restatement, each with one hold or release as its whole conversation (7 and 5 of 10). It then answered once the Turn's first model answer had ended with a Menu call, and the claim fell to 2 of 10. The cause is the standing Briefing, rewritten after the claim was written (ADR-0062): "If nothing is allowed, stop: the workflow prompts you again when something is." A correct model with nothing allowed now stops, so the in-Submission moment the claim measured is rare, and the model that skips it is the correct one. What the Briefing promises instead is the wake (ADR-0016), and wake measures that: the model stopped with nothing allowed; once its Submission has settled, the step answers the board; and the first Menu call after the wake is release or hold. A model that never stopped is void. Its floor is push-follow's, 9 of 10. push-follow keeps the restatement case: a list that changes while the model is still working. push-wake had no baseline cell.

    • noticed. The kill must not land while a Turn runs. The first Turn starts the server and picks started. Then the body waits at the kill Gate, and no Turn runs. The step checks that the server answers, with a probe that it execs in the Harness container. Then it execs the memory hog. It checks that the guard's list (GET /memory-kills) holds a new kill and that the port is dead, and it answers the Gate. The second Turn continues the same conversation, and its prompt must carry the memory-kill notice. The first Turn has ended, so it cannot see the kill. What the second Turn knows of the kill, it knows from the notice.

      Two earlier setups killed during the first Turn, and their runs showed why that is wrong. In 1 of 10 trials of two runs of 2026-10-12 (8374be56, 9d220d92), the first Turn saw its server die and did not pick started. On 2026-10-22, Haiku 5.5 often ended its first Turn before the kill, so the kill landed in the second Turn, and that Turn was not told. A first Turn that saw the kill often started the server again. Then the second Turn's report can come from what the first Turn saw, not from the notice. The driver made all of these trials void. The last of those setups polled the port from the host every 3 s during the first Turn, and it killed when the port answered. It made a trial void when the first Turn made a Working-tool call after the kill. On the home lab, 7 of 15 trials of that setup were void for that reason. All 8 measured trials passed. But 2 voids were past the bound, so the cell was 8 of 10, below its floor of 9. Qwen keeps working in its first Turn, sees the kill, and starts the server again. A kill between the Turns removes this mixed cause, and it needs no timing void.

      The new setup found a gap in the product. The guard put a kill on the stream of the Submission that ran. When no Submission ran, the Orchestrator did not hear of the kill, and the next Turn got no notice. Main fixes this (91699c24, 8c274330). The pod keeps every guard kill, and the Agent actor reads them before it admits a Turn into the Workspace. So the next Agent in the Workspace gets the notice once.

      Two voids stay, each a setup that did not happen. In the first, the first Turn does not pick started in its time: the run is live, and its body is in starting. In the second, no server answers at the kill Gate: a server that outlives its bash call is the model's job. A body that left starting for faulted (agent.fault) or lost (workspace.lost) invokes fail, and the run ends failed. It is not void, and it fails "every trial finished", as the finished rule above says. The bound stays half the trials, rounded up. Rejected: a larger bound for noticed, and a fault in starting counted as void.

      Two more voids are new. In the first, the kill does not take the server. The guard's list (GET /memory-kills, 91699c24) holds no new kill, or the port still answers after the hog. Then the exec fails, or the kernel's OOM killer takes the hog before the guard does. In the second, the kernel kills the container, not the guard. A kernel OOM kills the whole container, and the container's restart count goes up. The kubelet counts the restart 1 to 3 s after the kill. So the step reads the count right after the hog, and again after it reads the guard's list or that read throws. The guard's list is the backstop. A kernel kill leaves no new guard record, so when the rise does not show yet, the trial is the "no new kill" void. It is a void either way, never a miss. On main, a port that still answered after the hog was a miss. A dead port with no guard kill counted as a kill, so that trial could pass. Now each is a void, because the setup that the claim measures does not happen, and a port that is dead for another reason is not blamed on what jr2 tells the model. Each void cancels its run before the second Turn. Only the voids after the hog give the hog's exit code. The user approved these two voids on 2026-10-23.

      The prompt and the floor did not change. The pass changed: a trial now also needs the notice in the second Turn's prompt. Before, the step did not read the second Turn's prompt. Now the kill lands before the second Turn starts, so jr2 must tell that Turn. A prompt with no notice is a miss, and the note names it as a defect in what jr2 shows the model. The setup first ran on 2026-10-23, at an earlier version of this branch (09291511, not kept) on main 8837af28. That step named the Gate hold, had killed in the pass, did not read the guard's list, and had neither of the two new voids. It gave 10 of 10 on the home lab with no void, and 10 of 10 on Haiku 5.5. A later version of this branch (c220d895, not kept) scores a trial as this step does. On 2026-10-23 it gave 10 of 10 on the home lab, on Haiku 5.5 and on Sonnet 5.5, and no void fired. The user confirmed the 2026-10-12 void on 2026-10-12, and approved the setup with the Gate between the Turns on 2026-10-23.

    The Rest rule (ADR-0024) changed a third, mode-refusal, before any run under it. Its timer moved reviewing on to approved while the model composed its next call; under Rest that timer holds ask_review outstanding until it fires, so the stale list the claim needs never reaches the model and every trial would be void. The timer now has no target: it shuts the review window, and the guards of revise, submit and publish read it, so after it fires only publish is allowed in reviewing. The prompt, the floor and the pass are unchanged: the call after the correction receipt is publish. Both setups have run: mode-stop and mode-refusal cleared their floors 10 of 10. The user approved mode-refusal's setup change (e20cc0d) on 2026-10-11: a forced change that measures nothing new, with the prompt, the floor and the pass criterion unchanged, and 10 of 10 on 6f91bcc and on 9cd3935.

    On 2026-10-23 mode-refusal's setup changed as mode-stop's did, for the same cause: in Haiku 5.5 trial 38368e52 the model would not publish notes that it saw no one approve. A reviewer below reviewing approves the notes, and its output is ask_review's answer, with revise and submit allowed. The window's timer moved to reviewing.answered, the state that the reviewer's answer enters, so it starts when the answer is fixed, as it started at the receipt before. The answer reaches the Harness on the long-poll of the outstanding pick, not on the POST of the pick. The prompt, the floor and the pass are unchanged. The new text that the model reads is the same answer as mode-stop's, followed by "Your turn is not over. Allowed now: revise, submit." (in the model's tool names). A model that holds the approval and calls publish before the window shut is refused, and one that calls it after the window shut is taken. Neither trial refused a pick from the list, so both are void, inside the claim's bound. The user approved this change on 2026-10-23 (question 21). In the claim-setups run (below) it gave 10 of 10 on the home lab, on Haiku 5.5 and on Sonnet 5.5, and every trial finished.

  • Claude Haiku 5.5 and Claude Sonnet 5.5 (Anthropic API). The cells of features/model-baselines/anthropic--claude-haiku-5-5.json (18) and features/model-baselines/anthropic--claude-sonnet-5-5.json (20) come from the full runs of 2026-10-23 on main 8837af28, except the six cells that the claim-setups run gave (below). Haiku ran from 17:30 to 18:21 UTC, and Sonnet from 18:21 to 19:26 UTC. Both ran a version of this branch (c220d895, not kept). The later commits of the branch do not change how a trial is scored.

    • Haiku: 16 of 19 scenarios passed, every claim cell at 10 of 10 and each run at 1 of 1. Three scenarios failed at "every trial finished" and wrote no cell. mode-refusal: 9 trials passed. The 1 trial that missed (38368e52) is also the 1 that did not finish. mode-stop: 9 trials passed and 1 missed (0da530b9). That trial and 3 more did not finish (71a52a32, 08f40dc7, 1ed86b01). push-follow and push-no-refusal: 10 of 10 passed both cells, and 1 of 10 did not finish (5d3096da). An earlier version of this branch (09291511, not kept) ran Haiku push-follow on 2026-10-23 too, and 3 of its 10 trials did not finish.
    • Sonnet: 18 of 19 scenarios passed. Sonnet 5.5 has no off, so its off cells ran at effort high, and in this run its on cells asked for high too. Since 2026-10-23 the profile's on cell asks for medium, as Haiku's does (anthropic-sonnet.json; at pi-ai 1.4.0 Sonnet's thinkingLevelMap.medium is medium), so off and on are two conditions. The obedience/on, ticket/on and ticket-modes/on cells of this run measured high, so they left the baseline file, and the claim-setups run (below) measured each again at medium. The user approved this on 2026-10-23 (question 23). obedience/off gave 9 of 10, every other claim cell 10 of 10, and each run 1 of 1. In the miss (b2b5a38d), the prompt asked for ship and only hold was allowed. The model stopped with text, and after the nudge it picked hold. mode-stop failed at "every trial finished" and wrote no cell. Its 10 measured trials passed, and none of the 10 finished.

    In the unfinished mode-stop and mode-refusal trials, the model does not take a step that the claim's story says needs a review or an approval. That review or approval never comes. Then the nudges run out, and the run faults. The conversations show this:

    • Sonnet mode-stop (for example 5a0ba0c1): after the nudge, the model's thinking reads "Since no reviewer feedback has come in yet, submitting now would be premature". It picks revise and asks for review again. This repeats until the run faults. The workflow's prompt says "wait for the reviewer's answer", but no reviewer answer comes: the timer only opens the picks.
    • Haiku mode-stop: 3 of the 4 unfinished trials (71a52a32, 08f40dc7, 1ed86b01) have the same revise, ask_review loop. In 0da530b9, the model stops and says that neither pick fits, because the review has no answer yet.
    • Haiku mode-refusal (38368e52): "I'm not calling publish. The v2.3 notes have not been approved…"

    Haiku push-follow 5d3096da has a different cause. The desk answered four times: the ledger holds four approvals. After each answer submit was allowed, but the model picked revise to fix and check the failing tests ("Submitting now would ship code that fails its own tests"). The fourth revise came after the last answer that the step gives: the first answer and PUSH_REANSWERS (3) more. In coding, only ask_approval and run_tests are allowed, and no allowed pick ends the Turn. The model said "I've now requested approval four times without a response", and it stopped. The nudges ran out before the driver's cut at TRIAL_MS, so the run faulted.

    The home-lab Qwen takes the step anyway in mode-stop and mode-refusal. The "every trial finished" check counts these trials against the scenario, but the measured cell passes. On 2026-10-23 the user decided (question 21) to change the setups of these claims, and no floor or check. For mode-stop and mode-refusal, the review now has a real answer (the setup notes above). For push-follow, the step's limit on desk answers went: the desk now answers each time the coder is back in coding, until the trial's cut, TRIAL_MS from the first answer, which stays the bound. The rule that a fault before the cut is unfinished stays. The measures (the first pick after the first answer, and the first Menu call after the first restatement) and what the model reads do not change. These runs came before that change.

    The claim-setups run measured these changes on 2026-10-23, at snapshot 42433e48 of this branch on main 4cb2bc2e. Its cells were written from 01:47 to 02:22 UTC on 2026-10-24. It ran only the scenarios that the changes touch:

    • Home lab: mode-stop/off and mode-refusal/off, 10 of 10 each, equal to their baselines.
    • Haiku 5.5: mode-stop/off and mode-refusal/off, 10 of 10 each, and every trial finished. Before the change, 4 and 1 of 10 did not finish. Both are new baseline cells. push-follow and push-no-refusal passed 10 of 10 each, but 2 of 10 trials did not finish (90ab1ee4, 2b068c3a), so the scenario failed at "every trial finished" and wrote no cell. The desk answered each time a revise took the coder back to coding: twice in 90ab1ee4 and five times in 2b068c3a. Each run faulted within 30 s of the first answer, long before the cut, when the nudges ran out (agent completed its turn without a pick that ends it; still allowed: ask_approval, run_tests). The prompt asks only to run sleep 20 && ls and read the listing, and the Repo's tests fail. In 90ab1ee4 the model ran the tests, read the 2 failures ("The work isn't done, so submit is wrong"), picked revise, did not fix them, and stopped. In 2b068c3a the model made no change ("I won't call submit on empty work, and I won't request approval for a change that doesn't exist"), and it looped on revise and run_tests until the nudges ran out. So the model will not submit work that fails or that has no change, and it does not fix the tests.
    • Sonnet 5.5: mode-stop/off 10 of 10, and every trial finished (before the change, none of 10 did): a new baseline cell. mode-refusal/off 10 of 10 and obedience/off 9 of 10, equal to their baselines. At medium, obedience/on gave 9 of 10, ticket/on 1 of 1 and ticket-modes/on 1 of 1: new baseline cells. In each obedience miss (cfc238c8 off, b1c7a9a2 on), the model made no call before the nudge, then picked hold.

    The first full Haiku run, of 2026-10-22, used main at cd6e6145, with this tier's Anthropic profiles. Later main commits change what the model reads, among them ccd81ce9, eb76a3e4, 39a3610d, 8c274330 and 37bbf7ff. Others change how the Harness runs the tool calls of one answer, among them be347513, ecb8c556 and 9d37d732. So the cells of that run are not kept, and nor are the Sonnet 5.5 cells of a run on cd6e6145 (obedience and ticket only). Fourteen of nineteen scenarios passed. Five failed: noticed, a defect in its step, and the four below. The causes, read off each trial's evidence in features/.tmp/model-trials/anthropic--claude-haiku-5-5/:

    • noticed. The setup of this run killed during the first Turn, and its step did not check that the kill landed inside that Turn: a defect in the step. All the counts here come from that old setup. Haiku 5.5 often sends the server's bash call and started in one response, so its first Turn ends before the server listens. This run polled from the host every 3 s. Read with the later void rules of the old setup, 8 of its 10 trials are void. In 4, the model sent the call and started in one response. In 4 more, the kill landed in the second Turn. The 2 measured trials passed. Two diagnostic runs with a kill made in the pod sent the call and started in one response in 4 of 14 and 7 of 15 trials. A third such run gave 10 of 10, which is not valid, because that kill let a blind Agent pass. These trials also show a defect in what jr2 shows the model. Three of them (0e1ebfbf, 79c9a859, 92895334) and three Qwen trials read a false answer to started: "the pick never reached the Orchestrator (This operation was aborted)". Main fixes it (39a3610d).
    • mode-refusal, 7 of 10, below its floor of 9. In the three misses the model read the correction receipt, which named publish alone, and would not publish notes that it judged unreviewed: it called revise, which the correction had already refused (e06c7019, ab93b886), or it stopped and the nudges ran out (393058c3, faulted). In each, its text named publish as the one allowed pick, so it read the newest list; it refused the pick on the task.
    • mode-stop, every measured trial passed, and 9 of 10 did not finish (for example 7f3ed024, cae14640, 9cb4a03a). After the wake the model picked revise, an allowed pick. Then it called ask_review again, whose receipt allows nothing, and it stopped each time: the loop revise, ask_review, stop ran until the nudge budget faulted the run.
    • push-follow and push-no-refusal, every measured trial passed both cells, and 6 of 10 did not finish (586bdcf9, 9eae17a0, 855dd8ce, 1e78473f, 9b1d3c83, 565587a2). After a revise whose receipt said "…so it is not over yet" and allowed ask_approval and run_tests, the model ended its Submission with text ("I'll wait for the next prompt"), and the nudges ran out (agent completed its turn without a pick that ends it). The scenario failed at "every trial finished", so neither cell was written.
    • continue-after-signed-push, 4 of 10, below its floor of 9. Six trials were void past the bound (921a5999, 6ba4640a, a9aba70e, 600bb85a, ba0ac32c, a63db6b3): the first push published no signed copy. The Sandbox has no git identity, so the model's first git commit failed, and the model sent the commit and publish in one response, so publish pushed only init. The model then set an identity and committed, so its later pushes published.

    After that run, main gave the Sandbox's git a default author (7d7f7f5) and changed the "…so it is not over yet" receipt (eb76a3e). The run of 2026-10-23 measured each of the five again. noticed and continue-after-signed-push gave 10 of 10, and mode-refusal, mode-stop and push-follow are the three Haiku scenarios above.

  • The drivers' readings of the printed conversation, of a workflow's modes and of a restarted Harness container are plain functions in features/lib/model-said.ts, the readings of a trial's kept evidence are in features/lib/model-evidence.ts, and what each claim workflow means by "finished" is a table in features/lib/model-finished.ts, with unit tests beside them that need no cluster and no model.

  • The run has a second form, ticket-modes: one coder Turn walks three modes of its state to a verified commit.

  • A claim whose notice no step can yet raise (Compaction's summary, a kernel group kill) is added when the step exists; the ADR does not wait on it.

The docs of Kit version 0.5.1. Each page is also at its .md URL.