Problem
Nothing about an issue's gate state survives a run boundary. processIssue builds a fresh ctx every time:
settled: [] at :4821
metrics: { ..., pr_review_iters: 0, ... } at :4846
On the process_pr resume path at :4871, the run does worktree setup, pushes one "Resumed Run" decision, and calls reviewAndMerge(ctx), which starts its loop at iter = 1 against MAX_PR_REVIEW_ITERATIONS. Nothing reads a prior count, and settledBlock(ctx) renders empty into both review prompts at :4619 and :4639.
So a resumed run judges the PR with no record of what the contrarian gates adjudicated, and draws a fresh review budget. The reviewers are told to read issue and PR comments, which is a prose narrative, not a record.
runs.jsonl cannot fix this. buildLedgerLine at :5613 carries sixteen fields and gate_findings is not among them; the full per-issue detail lives in logs/<logs_dir>/runs/<run_tag>.json, which the engine never reads back. More decisively, logs/ is gitignored and the mill skill documents runs.jsonl as a per-host local artifact, so it evaporates in the cross-host resume case this exists to serve.
Change
This issue ships the substrate only. No consumer.
- Write. Post a structured block, behind the existing marker convention
<!-- ticketmill <repo>#<issue> -->, carrying { schema, issue, gate_budgets, settled }. Two boundaries are enough: once when the plan gate resolves, and once per pr-review iteration. Append-only, read last-wins, mirroring the outcomes.jsonl contract that diffOutcomeGrades already implements.
- Read. Add one step to the preflight probe (
:6905-6949) returning the raw blocks as strings, and a field on PREFLIGHT_SCHEMA (:478) to carry them.
- Parse. All parsing is pure JS above the
TICKETMILL-TEST-HARNESS-SPLIT marker at :6522. The agent returns raw text and never decides, matching the OUTCOMES_SCHEMA.prior_ledger_lines contract and the PIN documented in docs/architecture/metrics.md under "A quality-fix iteration closed a PIN violation".
Two constraints
The fail-open must be visible. The preflight probe runs at M.probe (haiku, low effort, :352), already carries six numbered steps, and on death returns a hardcoded default at :6947. Three states must be distinguishable downstream: block found and parsed, block genuinely absent (a first run), and read failed. A read failure that silently presents as "absent" would hand the eventual consumer a fresh budget, which is the exact silent cap exhaustion this substrate exists to close. Log the failure case.
Self-validate the write. Because the consumer lands a tier later, this issue would otherwise ship an unexercised write path into GitHub comments. Include a same-run read-back that logs a mismatch, so the substrate is proven before anything depends on it.
If a new agent stage key is added, add it to the M map at :351. stageOpts at :412 defaults unknown keys to { model: 'sonnet' }, so an unregistered key silently runs at sonnet.
Acceptance criteria
- The block is written at both boundaries and re-read across a simulated resume.
- The parser is unit-tested above the split marker with no
gh or git in the loop.
- Found, absent, and read-failed are distinguishable, and read-failed logs.
- Malformed JSON, a truncated block, and a block carrying a different issue number all fail open to today's behaviour rather than throwing.
- A run with no prior block produces byte-identical behaviour to today.
- The write is idempotent enough that a re-run does not corrupt last-wins reads.
node --test stays green. Add a dedicated test file; extend tests/decision-records.test.js if the settled ledger shape changes.
Out of scope
Nothing consumes the parsed state in this issue. Seeding ctx.metrics and ctx.settled, and enforcing an exhausted budget, is the follow-up that depends on this.
Documentation target
Document in CHANGELOG.md and in docs/architecture/gate-hygiene.md (create it if absent), adding its row to docs/architecture/index.md's file map.
Do NOT edit docs/architecture/pipeline.md, metrics.md, or failure-semantics.md. Their moved prose is hash-frozen by tests/architecture-provenance.test.js, and since the tech-docs stage runs at :4707, after the last test-loop execution, a frozen-prose edit surfaces only as a red CI check on the batch PR. New pages are authored text and are explicitly allowed; see docs/architecture/CLAUDE.md.
Problem
Nothing about an issue's gate state survives a run boundary.
processIssuebuilds a freshctxevery time:settled: []at:4821metrics: { ..., pr_review_iters: 0, ... }at:4846On the
process_prresume path at:4871, the run does worktree setup, pushes one "Resumed Run" decision, and callsreviewAndMerge(ctx), which starts its loop atiter = 1againstMAX_PR_REVIEW_ITERATIONS. Nothing reads a prior count, andsettledBlock(ctx)renders empty into both review prompts at:4619and:4639.So a resumed run judges the PR with no record of what the contrarian gates adjudicated, and draws a fresh review budget. The reviewers are told to read issue and PR comments, which is a prose narrative, not a record.
runs.jsonlcannot fix this.buildLedgerLineat:5613carries sixteen fields andgate_findingsis not among them; the full per-issue detail lives inlogs/<logs_dir>/runs/<run_tag>.json, which the engine never reads back. More decisively,logs/is gitignored and the mill skill documentsruns.jsonlas a per-host local artifact, so it evaporates in the cross-host resume case this exists to serve.Change
This issue ships the substrate only. No consumer.
<!-- ticketmill <repo>#<issue> -->, carrying{ schema, issue, gate_budgets, settled }. Two boundaries are enough: once when the plan gate resolves, and once per pr-review iteration. Append-only, read last-wins, mirroring theoutcomes.jsonlcontract thatdiffOutcomeGradesalready implements.:6905-6949) returning the raw blocks as strings, and a field onPREFLIGHT_SCHEMA(:478) to carry them.TICKETMILL-TEST-HARNESS-SPLITmarker at:6522. The agent returns raw text and never decides, matching theOUTCOMES_SCHEMA.prior_ledger_linescontract and the PIN documented indocs/architecture/metrics.mdunder "A quality-fix iteration closed a PIN violation".Two constraints
The fail-open must be visible. The preflight probe runs at
M.probe(haiku, low effort,:352), already carries six numbered steps, and on death returns a hardcoded default at:6947. Three states must be distinguishable downstream: block found and parsed, block genuinely absent (a first run), and read failed. A read failure that silently presents as "absent" would hand the eventual consumer a fresh budget, which is the exact silent cap exhaustion this substrate exists to close. Log the failure case.Self-validate the write. Because the consumer lands a tier later, this issue would otherwise ship an unexercised write path into GitHub comments. Include a same-run read-back that logs a mismatch, so the substrate is proven before anything depends on it.
If a new agent stage key is added, add it to the
Mmap at:351.stageOptsat:412defaults unknown keys to{ model: 'sonnet' }, so an unregistered key silently runs at sonnet.Acceptance criteria
ghorgitin the loop.node --teststays green. Add a dedicated test file; extendtests/decision-records.test.jsif the settled ledger shape changes.Out of scope
Nothing consumes the parsed state in this issue. Seeding
ctx.metricsandctx.settled, and enforcing an exhausted budget, is the follow-up that depends on this.Documentation target
Document in
CHANGELOG.mdand indocs/architecture/gate-hygiene.md(create it if absent), adding its row todocs/architecture/index.md's file map.Do NOT edit
docs/architecture/pipeline.md,metrics.md, orfailure-semantics.md. Their moved prose is hash-frozen bytests/architecture-provenance.test.js, and since the tech-docs stage runs at:4707, after the last test-loop execution, a frozen-prose edit surfaces only as a red CI check on the batch PR. New pages are authored text and are explicitly allowed; seedocs/architecture/CLAUDE.md.