ADR-0053.
the construction gate certifies a declared horizon, not the whole arc
Date: 2026-08-13 Status: accepted (2026-08-13; ratification proofs recorded below)
Context
The product review of 2026-08-12 named a usability hole that no other decision closes (UX hole
number 1). It is measured, not supposed. Verified live on the shipped 0.33.5 (echo $? on real
runs, not reasoned):
runward check --strictcollapses every non-filled deliverable across all phases into onegapscount (src/lib/verdict.ts:106-116), every gated-deliverable violation across all phases into onestrictGapscount (src/lib/verdict.ts:123-146), and the sole exit arithmeticverdictFrom(src/lib/verdict.ts:162-165) folds those into a single boolean: exit 0 only when the count over the whole arc is zero.- During construction, later-phase deliverables (Govern day-zero, Hand over) are by definition not
yet
filled, so a requiredcheck --strictin CI exits 1 continuously until the entire arc through handover is done. It turns green only at the very end.
So a team that wires the required check the way the docs prescribe (a hard status check on
check --strict, ADR-0028) gets a red gate for the whole build and learns to ignore it, or wires
it || true, or reconstructs a subset by hand from check --json | jq (every deliverable row
already carries {phase, state}). Each of those is a worse, untested answer to a real need: a CI
signal that is honestly green while the team is on track for the phase it has actually reached.
analyze() already names the furthest-green boundary (currentPhase, src/lib/mission.ts:146-162)
but never certifies it: there is no verdict, and no exit code, for a prefix of the arc.
This exact object was deferred to its own decision. ADR-0033 recorded, in its Alternatives, "the partially-filled-but-shipped case … needs a deterministic 'delivered' signal; its own decision" and refused to make Iterate a sixth gated rung precisely because such a rung would "never complete (permanent red) or complete falsely". This ADR is that deferred decision, taken now, under the constraint ADR-0033 set: the signal must not be a rung that completes falsely.
The naive form of the fix reopens the false-green family, and this was reproduced, not feared.
A check --through <phase> that simply narrows the counters to phases up to K, taken alone, was
attacked on the real code: scaffold init --example (strict-green), revert the three Govern and two
Handover deliverables to their raw templates (a genuine "crossed through floor, not through govern"
state), then measure. check --strict reads exit 1 (currentGate 5 · Govern (day zero), 16
conformance gaps, all of them in {Govern, Handover}: prompt-injection, secrets boundary, sandbox,
MCP pinning, resilience, eval, handover). The floor prefix is genuinely clean. So a bare
check --through floor feeds verdictFrom(0,0,0) and flips the exit code 1 → 0 while the entire
day-zero governance layer is a set of raw templates. Branch protection consumes only that exit
code; the honest caveats the command prints beside it do not reach it. That is the amendment of
ADR-0045 turned against the tool: "the run
is the exit code, not the sentence next to it." A construction verdict that can become the sole
required merge gate is the "completes falsely" trap in a new costume.
The worst case, named first: the true statement "the prefix through K is crossed" is a green that a required check reads as "shippable", and no exit code can distinguish the two — because on the fixed 0/1/2 contract a legitimate prefix crossing must exit 0. The tool cannot police how a human wires it. So the decision below is not a flag; it is a flag bound to a wiring contract, and the contract is the load-bearing half.
Decision
1. runward check --through <phase-id>: a declared-horizon prefix verdict. phase-id is a
presence-phase id in the ordered set {frame, architect, floor, govern, handover} (topology is
folded under architect, exactly as the presence view already does), reusing the phase-id vocabulary
already parsed and validated for rules --phase (src/cli.ts:122, src/lib/conformance.ts:25-33).
It composes with --strict and --json. It narrows only the set of phases fed to the existing
counters: countGaps and judgeGated filter their loops to phases whose ordered index is ≤ K;
the pure verdictFrom(gaps, strictGaps, hookFailed) is then called with those subset counts,
unchanged. The exit arithmetic is not touched.
What it certifies: every deliverable in phases ≤ K is filled, and (under --strict) every gated
deliverable in phases ≤ K has zero conformance, evidence and drift violations. What it refuses to
certify: anything about phases after K, which are surfaced as explicitly deferred, never as
crossed; and that the mission is complete.
2. The horizon is declared, never inferred. There is no --wip/--auto that detects K from the
furthest-green prefix. An inferred horizon would silently lower itself when a crossed phase broke,
exiting 0 at the smaller prefix and hiding the regression, the exact false green
ADR-0045 forbids. K lives in the
version-controlled, reviewed CI YAML, where a reviewer sees which horizon the team claims. The
declared horizon is a floor the whole prefix must hold: the phase-global integrity checks are
not scoped by --through — the corpus scaffold-lock (src/lib/verdict.ts:196-213), the
evidence seal (217-223) and the unratified-ADR check (225-227) still run over the whole mission,
so a moved corpus, a broken seal, an edited or removed rule, or a DRAFT/hypothesis ADR reds the
verdict at any horizon. Regress anything at or below K and the exit goes to 1. This property was
adversarially confirmed to hold on the real code.
3. The wiring contract, which the ADR carries because the tool cannot enforce it. The
release / merge-to-main gate is always the full check --strict (no --through). --through is
the construction-branch progress signal, aligned 1:1 with governance through phase K
(ADR-0028); it is never the sole
required status check that guards production. The tool cannot mechanically prevent a team from wiring
check --through floor as their only merge gate — a required status check keys on an exit code, and
no deterministic, branch-blind, zero-LLM tool can know a green is being read as "done". So the mode
does the only honest things it can at the mechanism level, and states the rest as contract:
- it refuses
--freeze(check --through <id> --freezeexits 2, misuse): a seal certifies a full crossing, and a partial arc must never be sealed as a completed mission (src/commands/check.ts:280-282already refuses to seal an empty set); - it refuses to infer its own horizon (decision 2);
- it prints a loud horizon line naming the deferred phases and stating, in the run, "prefix
verdict through
<phase-id>; N later deliverable(s) deferred; this is not a completion verdict"; --jsoncarriesthrough,horizon.deferred[]andgaps.deferred, so any tooling can detect a prefix verdict and refuse to treat it as a whole-arc gate;currentGateandsteadyStatekeep their true whole-arc values.
This is not weaker than the status quo: teams already build partial-green by hand with jq, which
drops the phase-global corpus/seal/unratified checks; --through keeps them. It is a
disciplined, tested version of a thing that otherwise ships as untested CI glue, off the seam a test
can reach (ADR-0047).
4. The exit contract and the JSON contract are untouched. No fourth exit code: --through maps
onto the fixed 0/1/2 port (ADR-0012,
ADR-0011,
ADR-0030); unknown
phase-id and --through+--freeze both take the existing misuse path to exit 2
(src/cli.ts:148-159). The new JSON fields are additive-only (through, horizon, gaps.deferred);
nothing is renamed or repurposed. --through handover (the whole arc) is identical on the exit
code to plain check --strict; it is not byte-identical on --json, which additionally
carries through:"handover", horizon.deferred:[], gaps.deferred:0 — the identity test is scoped
to the exit code, never the JSON bytes.
Alternatives considered
- Close it by doctrine only, no flag: require
check --strictat release, run non-requiredcheckfor visibility during construction. This has zero false-green vector because it never claims a green mid-construction; it just does not gate. It is the honest fallback and the ADR does not foreclose a team choosing it. It is carried here as a live option because it removes a product surface and a residual misuse risk; the reason to prefer the flag is that "advisory, ignore the red" trains teams to ignore the gate, and a meaningful, honestly-green construction signal is what the review found teams actually want. This is the one genuine fork for the author: ship the flag with its contract, or close UX hole 1 by doctrine alone. The rest of this ADR assumes the flag; if the author chooses doctrine-only, decisions 1–4 collapse to adocs/pattern and the ratification below is replaced by a documentation test. check --phase <id>(single-phase, reusing therules --phaseflag). "In good standing at the phase we reached" is a prefix property (all phases up to K), not a single-phase one. A single-phase pass can go green on K while an earlier phase regressed — a stale pointer in frame/architect would be invisible to--phase floor. That is the regressed-crossed-phase false green again. Honest only if it also asserts the whole prefix, at which point it is--throughunder another name.- An auto-detected
--wipmode, K inferred from the furthest-green prefix. Inferring K lets the tool choose its own horizon; when a crossed phase regresses the prefix simply shrinks and the mode still exits 0 at the smaller prefix, hiding the regression. Rejected on decision 2. - A fourth exit code (
3= "in progress but healthy"). Forbidden: the 0/1/2 contract is a fixed, versioned, additive-only port (ADR-0012, ADR-0030); a fourth code breaks every consumer that reads any non-zero as failure, including CI required checks and the thin shells docs/distribution.md documents. - No flag, document the
check --jsonprimitive and ajqrecipe. Reconstructible today, but it relocates the verdict into per-team CI glue no runward test reaches, reopening the untested-verdict problem the extraction ofverdict.tsclosed (ADR-0047); each team'sjqdrifts and silently drops the phase-global checks. Kept as the documented fallback, never the primary answer. - Fold the horizon into a recorded mission-side marker (a "we are at phase K" state in the seal). Introduces mission state a construction branch must commit and keep fresh, duplicating what a declared flag in the reviewed YAML gives for free; and seal semantics certify a full crossing, so a marker in the seal reads like completion — the precise false green to avoid.
Consequences
- Exit 0 stops implying
steadyState. Today exit 0 ⇔ the whole arc is clean.--throughmakes exit 0 coexist withcurrentGatenaming an open phase andsteadyStatefalse. On the fixed 0/1/2 contract a legitimate prefix crossing must exit 0, so this is inherent — which is exactly why the contract (decision 3), not the exit code, forbids--throughfrom being a required release gate. The migration note names this and the population it affects (teams adopting the flag), in the version that ships it. - The residual misuse risk is real and named. A team can wire only
check --through flooras their required merge check and ship a govern-less, handover-less product behind a permanently green gate. The ADR states the contract, the run states the horizon loudly, the JSON exposes the prefix, and--freezeis refused — but a human who wires a progress signal as a release gate is misusing it, the same way a human who makes only unit tests required ships untested integrations. runward's own doctrine already holds this line: the gate verifies presence, pointers and integrity, never that a human wired it wisely; usage stays human. - Every regression at or below K still reds. The phase-global integrity checks are unscoped, so the declared horizon is a floor, not a ceiling — verified adversarially, not asserted.
- The topology fold is load-bearing.
topologyis aGATED_DELIVERABLESentry with no presence-phase-id; an implementation that filtersjudgeGatedby string-matching the presence-id would silently exempt the topology manifest at K = architect — a false green inside the certified prefix. The implementation maps the ordered index explicitly and the ratification proves, both directions, that a brokenexecution-topology.mdreds at--through architect. - Cost: about 2.5 to 3 maintainer-days, in line with ADR-0051's estimate for a comparable
verdict-semantics change.
verdict.ts: thread an optionalthroughindex into the two counter loops, leavingverdictFromand the global corpus/seal/unratified blocks untouched (~0.5d).cli.ts: the flag, the phase-id validator extended from therules --phaseset, the--through+--freezemisuse guard (~0.25d).check.ts: pass-through, the horizon line and deferred list, the additive JSON fields (~0.5d). Tests both directions (~0.75d). ADR authoring with its fact-check pass and the ROADMAP entry (~0.5d). No third-party or external cost.
What this does not claim
- Not that
--throughis safe to use as a release gate: it is not, and the whole of decision 3 exists to say so. It closes the usability hole for the construction branch, not the release gate. - Not that the tool can prevent its own misuse: a deterministic, branch-blind gate cannot know a green is being read as "done". It makes the honest signal available and states the contract; the wiring stays a human decision.
- Not that anything changes at the seal, the six phases,
GATE_NON_SCOPE, or the 0/1/2 contract: all unchanged;--throughnarrows the phase set fed to the existing arithmetic and adds JSON fields, nothing more. - Not that this ADR crosses a phase: it is cited by no manifest and crosses no phase
(ADR-0045 decision 4).
node dist/cli.js check --strictmeasured exit 0 on this repository on 2026-08-13 before this file; it must read exit 0 after.
Ratification
Accepted (header, 2026-08-13). The bar it had to clear — all of the following passing in CI, on the built binary, each test red before its change and green after (the ADR-0047 / ADR-0051 pattern):
- Prefix green mid-construction. On a mission crossed through floor but with Govern/Handover as
raw templates,
check --through floor --strictexits 0, while plaincheck --strictexits 1; the run names the deferred phases and--jsoncarriesthrough,horizon.deferred(non-empty),gaps.deferred > 0,currentGate= the first open phase,steadyState= false. - The floor holds, both directions. With the same horizon, reverting a phase-≤-K deliverable to
its template, editing a rule file, breaking the seal, or dropping a
DRAFTADR each redscheck --through floor --strict(exit 1). The topology case is explicit: a brokenexecution-topology.mdredscheck --through architect --strict, and is green only when the topology manifest is sound. - Contract guards.
check --through <id> --freezeexits 2; an unknownphase-idexits 2. - Whole-arc identity on the exit code. On a strict-green mission,
check --through handover --strictandcheck --strictreturn the same exit code (0); the identity test asserts the exit code only, not the JSON bytes. - Global invariant.
node dist/cli.js check --stricton this repository exits 0 before and after;test/unit/no-overclaim.test.jsstays green (this ADR argues against false greens, it does not assert one).
Ratification record (2026-08-13). All five proofs pass on the built binary, in
test/unit/verdict.test.js (ten cases under the "ADR-0053" heading). Red-before / green-after was
demonstrated by measurement, not asserted: with the phase scoping neutered (countGaps and
judgeGated fed null instead of the resolved horizon), the three cases that assert the new
semantics — the UX-hole-1 prefix certification, the topology fold, and the additive --through --json payload — turn red, while the cases that assert preserved invariants (the floor still reds
below the horizon, the global corpus / seal / unratified checks still red, the whole-arc identity,
the fail-loud on an unknown id, the CLI misuse guards) stay green; restoring the scoping turns all
ten green. node dist/cli.js check --strict on this repository read exit 0 before and after. The
mechanism ships in the release that carries this file; until that release, the flag is present on
main under this accepted, tested contract.
Reevaluation trigger (mandatory, dated)
Trigger set on: 2026-11-05.
The decision is wrong and must be revisited if any holds: a --through green is presented anywhere as a completion verdict despite the banner and the horizon field (the misuse this ADR spends its design on preventing); a team makes --through their only required check, which the wiring contract forbids; or the phase-prefix arithmetic is found to scope a check that should have stayed global.
Watched via: the conformance-gate incident log, and any consumer wiring reported to the project.
References
- ADR-0033 — deferred this exact signal to "its own decision"; this ADR is that decision
- ADR-0045 — the anti-paperwork doctrine and "the run is the exit code, not the sentence next to it", the false green decision 3 exists to refuse
- ADR-0012 / ADR-0011 — the fixed 0/1/2 exit port, unchanged
- ADR-0030 — the additive-only
check --jsoncontract the new fields respect - ADR-0028 / ADR-0029 — the CI required-check tier
--throughmust not become the sole gate of - ADR-0047 — the verdict stays on a seam a test can reach, which the jq-wrapper alternative fails
- ADR-0051 — sibling product-review decision on the gate's depth; independent axis (depth vs horizon)
- ADR-0052 — the first-pilot dependency that hits this hole on day one