feat(policy): the punt sweep gets an exit code - #817
Conversation
`stop_nudges` rule 5 has asked the right question at the end of every turn since CLOUD-1051 — is this row genuinely independent work, or a punt you could close here? It is a nudge, so an agent reasons past it: answering wrongly costs nothing and the answer dies with the turn. Measured 2026-09-01: four deferrals in one session, each with a principled-sounding blocker, every one of them false. Three were invisible to `filed-over-own-diff` because their §1 named paths outside the diff, which `cites_only` exempts by design and rightly so for a refusal about proximity. The fourth was caught only because it named the diff. The detector was a human asking twice. `filed-and-left-open` is the third arm. It reads the rows this branch put on the board, subtracts the ones the PR body closes and the ones recorded before the branch's base, and reports what is left. It classifies nothing: whether a spin-off was legitimate is still the judgement no gate makes, and the author still makes it — in an admission whose articulation CLOUD-1278 binds into the commit message, where a reviewer reads it. CLOUD-514 ruled this half out in terms that were right on the day. The premise is obsolete rather than the reasoning: the shape it lacked — deny over an object, with the only exit an explanation written into history — exists since CLOUD-1051 and CLOUD-1278. Partitioned, not nested. `filed-over-own-diff` requires `not cites_only(id)` and this requires `cites_only(id)`, so no row earns both and a reviewer never sees two findings for one cause. Drafted without that requirement the arm was strictly wider than the proximity one, which broke the module header's own invariant. Three could-not-looks guard it, each a different question: an unread PR body (the closing remedy has nowhere to be written yet), an empty diff (a branch holding nothing open deferred nothing), and a record with no §1 column (the partition cannot be evaluated, so the row stays judged as before). `closes -` and `closes 0` stay distinct, which is what `zero-is-a-count` exists for. Refs: CLOUD-1311 Admits: 067783fc36bd1a49703ad0dc249fb95875ef1f9f60e06505e51a155e603e4509 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: policy/filed-here.rego Admits-head: be5a33c Admits-epoch: 781d35649dedb150ccb69e890c6aa482c7393233f5ffe1dce100609afd6150fb Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: CLOUD-1311 cannot be built at all. The punt sweep stays `stop_nudges` rule 5 — a nudge with no exit code — which is the defect measured on 2026-09-01: four deferrals, each with a principled-sounding blocker, every one of them false, and the detector was a human asking twice rather than any mechanism. Admits-answer-precondition: A registered .rego module has no owning verb: the module file IS the surface that declares the predicate, so there is no route that adds a third `violation` arm to policy/filed-here.rego except writing the file. The write lands in a reviewed PR for CLOUD-1311, where `mise run policy-test`, the compiled tier in crates/batten/tests/it/filed_here.rs and the declared `#MUTANT` rows all judge it before it can bind anything. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because no such surface exists for this object: batten declares no verb that writes a predicate into a registered module, and the redirect's own remedy is to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed or retracted — this is an addition to a module that keeps both existing arms intact, with the second arm narrowed only so the third cannot subsume it. Stated for the reviewer because it is the reason this class is protected at all: the arm being added judges the author who is adding it, so the diff and not my account of it is the thing to read. Admits: 4265150829ab6e4bb6ac8d3976cda321d94ca094835872fd4dceb6759672c6c1 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: be5a33c Admits-epoch: 781d35649dedb150ccb69e890c6aa482c7393233f5ffe1dce100609afd6150fb Admits-author: alec@wenzowski.com Admits-prev: bc0321f2fc7cbb652a826c1d4f8928972e3d7c212a817ed94c9af37e90c74973 Admits-answer-lost: CLOUD-1311's arm is unloadable and therefore unlandable. The module edit already made is dead code until this row exists, so declining here leaves the tree strictly worse than not starting: a module that fails to load disarms `filed-unrefined` and `filed-over-own-diff` too. Admits-answer-precondition: A `[[verdict]]` row is only expressible in batten.toml: the registry IS the surface, and `policy/filed-here.rego`'s new arm cannot load at all until `V-FILED-AND-LEFT-OPEN` is declared there — a module raising a token no row declares is refused at load. So the write to the authority is not merely the shortest route, it is the only one, and it lands in the reviewed PR for CLOUD-1311 where `mise run config-lint` and `mise run batten-check` judge it. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a verdict class — there is no verb that registers one, and the path's own redirect says to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed: this adds one `[[verdict]]` row with its routes and touches no existing row. The write is additive and strictly raise-only — a new deny class with a declared override precondition — so it cannot weaken any gate, which is the property house-style §8 asks of a config change and the one a reviewer should check in the diff.
Two cases failed on one cause, and it is the fixture rather than the arm: `install_module` writes `policy/filed-here.rego` into the scratch working tree and `base_delta` walks the tree rather than the index, so every fixture here has a non-empty delta by construction. `a_branch_with_no_diff_judges_no_row` therefore cannot exist at this tier. It stays as the module's own `test_` rule and this file records why the compiled tier cannot hold it — the mirror of the rule that a `with input as` case cannot prove the engine builds a shape. Deleting the assertion without the note would have left the guard untested in both tiers while looking covered in one. `a_row_recorded_before_the_file_was_touched_is_still_caught` was asserting silence before the touch. With the third arm that record is a row left open, so the case now asserts the row MOVING between the two arms — `filed-and-left-open` before the file is touched, `filed-over-own-diff` after — exactly one finding either side. That is the partition on one record, which is a better statement of the property than the silence it replaces. 3753/3753 green. Refs: CLOUD-1311
CLOUD-1311 The punt sweep is a NUDGE, so an agent reasons past it — CLOUD-514 called this ungateable, and the admission mechanism that landed since makes it gateable without judging
Why
It is a nudge. It has no exit code, and an agent reasons past it. Measured over one session, 2026-09-01: four deferrals, each with a principled-sounding blocker, every one of them false —
None of the four was caught by a mechanism. Three of them were invisible to What actually held, and it is the whole design inputOne refusal in that session could not be argued with: That is the signal. A punt does not survive because the reasoning is good; it survives because there is somewhere to put reasoning that costs nothing and is discarded when the turn ends. Why CLOUD-514's exclusion no longer bindsCLOUD-514 shipped the record this depends on and explicitly ruled the remaining half out:
True on 2026-08-13, and the reasoning is right: non-negotiable rule 3 forbids a gate resolving to a model verdict, and no predicate separates a punt from an honest split-out. The premise is now obsolete, because the gate does not have to decide. CLOUD-1051 replaced override passwords with issued capabilities and CLOUD-1278 made the articulation durable by putting it in the commit message, hash-bound and verified by That is also what makes the false-positive worry dissolve. An honest split-out is never refused — it costs one admission naming the row and saying why it is independent work. CLOUD-514's own §2 already argues this direction: "the friction must sit only on the impulsive path." An articulation a reviewer will read is friction on exactly that path, and free on the other, because a row you genuinely could not close has an answer ready. Refinement — Ready Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found when the repository's owner pointed out that the sweep is a nudge while the repository already has the mechanism for making an agent explain why a rule should not apply — and that the explanation gets written into durable history forever. CLOUD-1231 The claim receipt holds ONE key per branch, but `closing-key-check` expects a PR to close several — so a multi-row branch can only ever record a claim for one of them
Why Two gates disagree about how many rows a branch may serve.
The claim receipt is not. Minting a claim for a second row on the same branch overwrites the first. There is no append, and the shape has no room for a second key. Measured, 2026-08-31 on PR #770That PR closes CLOUD-1145, CLOUD-1161 and CLOUD-1218 — a shape Claiming CLOUD-1218 by hand would have erased CLOUD-1161's claim, so I did the board half (Todo → In Progress, assigned) and left the receipt alone. That is the right call for the tree and the wrong outcome for the record: the branch performed work for a row whose claim it cannot show. **Nothing failed, and that is the finding. ** Why this is not CLOUD-516's or CLOUD-733'sBoth are about the receipt being keyed by branch name — a stale claim inherited after a merge (516), and a rename stranding it (733). Both are now Done. This is a different axis: the key is fine, the arity is one where the workflow's own gate expects many. CLOUD-502 asked a related board question (a bundle landed as one PR cannot be represented) and was cancelled. This row is narrower and mechanical rather than about WIP accounting: it is about what the receipt file can hold. Refinement — Ready (let a branch's claim record every row it serves) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found while landing PR #770: the branch served three rows, could record a claim for one, and no gate noticed. |
|
Warning Review limit reachedNext included review available in 35 minutes. View limit detailsLimit details: You’ve used the included review currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (5)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (16)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe PR adds branch plan recording through Merge Risk: ⚪ Minimal · up to The PR adds an exit-code path for detecting filed-but-left-open work while preserving existing judgment and guard behavior; no actionable merge-blocking risk remains after normal checks and review. Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
The branching model is one commit one issue, one branch as many issues as the work needs, one PR everything done with no punts. Nothing in the instruction surface said so, and the one sentence that came near it said the opposite: `.claude/rules/toolchain.md` explained the branch-keyed claim receipt as attesting "a decision about an ISSUE that every commit on the branch continues to serve" — singular. The mechanism agreed with the wrong prose. `mint` takes a SLICE and line 1 of the receipt has always been an id LIST, so the many-row shape was expressible in one invocation — but the write is `fs::write`, so a second `claim check` INVOCATION replaced the first row's claim and said nothing. Measured 2026-09-01: reading that sentence, an agent declined to pull a second row onto an open branch and reported the storage key as the rule. So the ids union, guarded by the recorded base. A restarted branch (`git checkout -B <name> origin/main`) keeps the receipt because the file is keyed by NAME, and carrying ids across a changed base is exactly the stale claim CLOUD-516 measured sitting through four unrelated stories — so a changed or unresolvable base starts a fresh list. Could-not-look drops the list rather than carrying it: a lost claim costs one re-run, a carried stale one is the defect. Four cases, over the real `mint`: the join, no duplicate on re-claim, a restart resetting, and an unresolvable base carrying nothing either way. AGENTS.md now states the model where it binds every turn, with both failure modes named — a branch per row is the batching `land`'s lap loop exists to prevent, and stopping at one row is the punt `filed-and-left-open` prices. Refs: CLOUD-472 Admits: 4265150829ab6e4bb6ac8d3976cda321d94ca094835872fd4dceb6759672c6c1 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: be5a33c Admits-epoch: 781d35649dedb150ccb69e890c6aa482c7393233f5ffe1dce100609afd6150fb Admits-author: alec@wenzowski.com Admits-prev: bc0321f2fc7cbb652a826c1d4f8928972e3d7c212a817ed94c9af37e90c74973 Admits-answer-lost: CLOUD-1311's arm is unloadable and therefore unlandable. The module edit already made is dead code until this row exists, so declining here leaves the tree strictly worse than not starting: a module that fails to load disarms `filed-unrefined` and `filed-over-own-diff` too. Admits-answer-precondition: A `[[verdict]]` row is only expressible in batten.toml: the registry IS the surface, and `policy/filed-here.rego`'s new arm cannot load at all until `V-FILED-AND-LEFT-OPEN` is declared there — a module raising a token no row declares is refused at load. So the write to the authority is not merely the shortest route, it is the only one, and it lands in the reviewed PR for CLOUD-1311 where `mise run config-lint` and `mise run batten-check` judge it. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a verdict class — there is no verb that registers one, and the path's own redirect says to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed: this adds one `[[verdict]]` row with its routes and touches no existing row. The write is additive and strictly raise-only — a new deny class with a declared override precondition — so it cannot weaken any gate, which is the property house-style §8 asks of a config change and the one a reviewer should check in the diff. Admits: 5e94b114846dc2b86da06b29535aa4639351988b9ccd597ade5e803363af7ef2 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: 8cc3fed Admits-epoch: 78cca6d566d252da1ec49eae537f22670ac4ff47cb79a3a4d52e2451ff71c412 Admits-author: alec@wenzowski.com Admits-prev: 4265150829ab6e4bb6ac8d3976cda321d94ca094835872fd4dceb6759672c6c1 Admits-answer-lost: CLOUD-472's ratchet cannot load, and `ready lint` is left refusing every payload on an unresolvable grammar token. The escape stays open: the claims object that CLOUD-453 built and CLOUD-418 gave its `mutation` field remains opt-in, so a §7 naming three obligations in prose keeps linting green — measured 2026-09-01 on CLOUD-1306 and on CLOUD-1311's own block. Admits-answer-precondition: A `[[pattern]]` row is only expressible in batten.toml — the registry IS the surface, and `Grammar::assemble` resolves `ready-prose-dialect-exempt` by id with a LOUD failure, so `batten ready lint` cannot run at all until the row exists. The threshold is a consumer fact about this repository's own key space (non-negotiable rule 1), so it could not live in the crate even if there were a route. It lands in the reviewed PR for CLOUD-472 where `mise run config-lint` and the compiled tier in crates/batten/tests/it/ready.rs judge it. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a `[[pattern]]` row; no verb registers one, and the path's own redirect says to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed — this adds one pattern row and touches no existing one. The row is deliberately set ABOVE every key that exists today, so it refuses nothing currently on the board and cannot darken the ready frontier the way CLOUD-858 measured; that is the property a reviewer should check in the diff, since a threshold set too low is the one way this change does harm. Admits: 051b1f7234d5470e5dc0cfa2d30dcc6d57924594018ed39c45f34a1fe972f4ec Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: .serena/memories/workflow/board-states.md Admits-head: 8cc3fed Admits-epoch: 08df746a010060ba1c781d4de6935d9751f3cf01839b0aa0ca5c09e4982d7775 Admits-author: alec@wenzowski.com Admits-prev: 14eb1973fd24f18fe1092e42014b0cac84beef053e935f202b0aab519cb80b01 Admits-answer-lost: The correction stays a one-line rule with no record of why it was needed. Both measured failures — the singular sentence in `.claude/rules/toolchain.md` that pointed the wrong way, and `claim::mint` silently replacing the previous row's claim — would be undocumented, so the next reader re-derives the wrong model from the same two sources that produced it this time. Admits-answer-precondition: The memory IS the owning surface for this content: AGENTS.md is at its `policy-budget` ceiling (measured this commit at 3618 tokens of 3500, which refused the first attempt), so the rationale for the branching model cannot live there and the repo's own split puts on-demand content in `.serena/memories/`. The write was made through `mcp__serena__edit_memory`, the route the redirect names, so this records the change rather than authorising a route around it. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is what was taken, not rejected: the edit went through Serena's `edit_memory`, and this admission exists because `commit check` records every protected path in the diff regardless of the route that wrote it. R-RESTORE-IT does not apply because nothing was destroyed — the edit inserts two paragraphs ahead of the `claim-check` ordering section and changes no existing sentence.
CLOUD-453 built a fenced claims object whose `REQUIRED_CLAIMS` forces `tests`, and CLOUD-418 gave every entry a `mutation` field — an obligation that cannot name the mutation which would kill it cannot be written. That mechanism landed and was UNREACHABLE: `check_claims` returns false on an absent fence and the caller drops to the prose path, so the whole thing was opt-in. Measured 2026-09-01 over the 50-row Todo queue: nothing used it. CLOUD-1306, filed that day, carries a §7 naming three obligations in prose, none joinable to anything. `ready.rs` has emitted `dialect prose` per run all along, with a comment saying the corpus would "converge deliberately instead of in one sweep" — the sensor was right there and nothing ratcheted on it. A RATCHET, NOT A FLIP. `graph-check` enforces `Todo => ready-lint exits 0`, so refusing every prose block at once takes the whole ready frontier dark in one step, which is CLOUD-858's measured shape. TWO WRONG SURFACES PRECEDED THE RIGHT ONE, and both are recorded where the next author will look. First a `[[pattern]]` row spelling the exempt range as a regex over the key: the registry gives one concept one spelling and arithmetic is not a concept, and it decides on key TEXT, which this consumer already declares `ready-issue-mention-markup` for because the tracker rewrites a bare key into `<issue ...>` markup. Then a key ORDINAL, which reaches no consumer literal and still requires keys that are numeric AND monotonic with creation order — true of three popular trackers, false of a slug- or UUID-keyed one, where it would resolve to nothing and fail SILENTLY. A creation instant assumes nothing. Every tracker stamps one, the payload already carried it, and `policy/filed-here.rego`'s `predates_the_branch` already compares tracker timestamps this way: both sides fixed-width ISO-8601 UTC, so lexical order is chronological order. Three gates caught the plumbing rather than me. `resolve` refuses a field with no declared provenance layer; `trust` refuses a config field that does not say what weakening means for it; `policy-budget` refused the first attempt to state the branching model in AGENTS.md at its own ceiling. The weakening kind is `ready-cutover-relaxed` — later exempts more rows, and dropping the key is that move taken to its limit, since absent reads as could-not-look. Also here, from the same review: `.claude/rules/toolchain.md` states the retirement of by-path hook registration as a DIRECTION rather than a one-off, because a capability declared under a harness's own directory exists for one of five wired harnesses and is invisible to the other four; and `.claude/rules/policy-modules.md` records both wrong surfaces above as a rule about never building a predicate on text a round trip rewrites. 3762/3762 green. Refs: CLOUD-472 Admits: 62e398b8a6bd7a69ad128dc0dbc985bd085f8b36c29b27adf10386a855195e87 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: e1b0428 Admits-epoch: 08df746a010060ba1c781d4de6935d9751f3cf01839b0aa0ca5c09e4982d7775 Admits-author: alec@wenzowski.com Admits-prev: 5e94b114846dc2b86da06b29535aa4639351988b9ccd597ade5e803363af7ef2 Admits-answer-lost: The threshold stays spelled as `^CLOUD-([0-9]{1,3}|1[0-3][0-9]{2})$`, which is the wrong surface on two counts the repository already knows about: the pattern registry exists so one CONCEPT has one spelling and arithmetic is not a concept, and the decision would turn on key TEXT that the tracker demonstrably rewrites — this file declares `ready-issue-mention-markup` precisely because a bare key returns wrapped in `<issue …>` markup. Admits-answer-precondition: Both halves of this edit are only expressible in batten.toml: it removes the `[[pattern]] ready-prose-dialect-exempt` row and adds the `[ready]` table that replaces it, and neither a pattern row nor a config table has any owning verb — the file IS the surface. `Grammar::assemble` resolves pattern ids with a LOUD failure, so leaving the stale row while the code no longer reads it would be dead config, and removing it without adding `[ready]` leaves the ratchet unreachable. It lands in the reviewed PR for CLOUD-472 where `mise run config-lint` and the compiled tier judge it. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for both a `[[pattern]]` row and a config table; no verb registers either, and the path's own redirect says to change it in a pull request. R-RESTORE-IT does not apply in the usual sense but half of this edit IS a restore: it removes a row added earlier in this same branch rather than one that ever landed, so the net effect against origin/main is one new `[ready]` table. The threshold is set above every key that exists today, so nothing on the board is refused and the ready frontier cannot go dark the way CLOUD-858 measured — that is the property to check in the diff, since a threshold set too low is the one way this does harm. Admits: e21549fcda550f97b7487fa035daa0f02606801a168f44f2f5b8ec7c0cc74feb Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: e1b0428 Admits-epoch: 3a81b526ea895c28ff9a18819a9251bedd1f695f8c1146f88ca33f86464cdddb Admits-author: alec@wenzowski.com Admits-prev: 62e398b8a6bd7a69ad128dc0dbc985bd085f8b36c29b27adf10386a855195e87 Admits-answer-lost: The prose-dialect ratchet cannot be reached at all. `Grammar::with_prose_threshold` receives `None`, which is could-not-look by design, so `ready lint` exempts every row and the claims object stays opt-in — the exact state measured on 2026-09-01, where the mechanism CLOUD-453 built and CLOUD-418 gave its `mutation` field was used by nothing across the whole 50-row Todo queue. Admits-answer-precondition: This is the second half of one replacement and the file IS the surface for it: the `[[pattern]]` row was removed under the previous admission, and `[ready] prose_dialect_exempt_below` is what the code now reads. No verb registers a config table, and leaving the tree between the two writes is strictly worse than either end state — the ratchet would be unreachable while `config.ready` is `None`, which reads as could-not-look and exempts every row. Split across two admissions only because each covers one write; it should have been one edit, and that is my error rather than a property of the change. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a config table; there is no verb that writes one, and the path's own redirect says to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed — this adds one table and touches no existing one. The value is set above every key that exists today (highest live row CLOUD-1311), so nothing on the board is refused; a reviewer should check that number specifically, because setting it too low is the one way this change takes the ready frontier dark the way CLOUD-858 measured. Admits: 8164d00fb3d38d7017b27ed86e88026385f9529023dee50bf1b1ffa6afb28753 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: .serena/memories/workflow/agent-fanout.md Admits-head: e1b0428 Admits-epoch: 04d65e929fcb8962b43da408ccb71d1f8d5a63c839a9492142bf7009f372e12a Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: Three corrections stay unrecorded and get re-derived wrongly, as they were this session. The WIP cap keeps reading as a bound on tickets rather than on builds, so an agent splits work that belongs in one PR. Bundling keeps being justified by rebase amortisation, which the same file's caps section already refutes — a rebase costs no model turn, so there is nothing to amortise — and that argument gets weaker as automation improves, which is the tell it was never the reason. And nothing records that this repository caps no PR size, so an invented threshold recurs. Admits-answer-precondition: The memory IS the owning surface: AGENTS.md is at its `policy-budget` ceiling (3618 of 3500 tokens, measured when it refused this session's first attempt), so fan-out rationale cannot live there, and the repo's own split puts on-demand content in `.serena/memories/`. The write was made through `mcp__serena__edit_memory`, the route the redirect names; this records the change rather than authorising a route around it. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is what was taken, not rejected: the edit went through Serena's `edit_memory`, and this admission exists because `commit check` records every protected path in the diff regardless of the route that wrote it. R-RESTORE-IT does not apply because nothing was destroyed — the edits replace two wrong rationales with the measured ones and add the maximise-the-bundle direction; every existing measurement, including the owner-set caps of 6 and 2, is left exactly as it stands.
|
A fifth measurement for CLOUD-1311, from #811's session, and it names a route this arm does not reach.
I served CLOUD-1213, did not close it, and declined it with
So the asymmetry this PR is closing has a second half. Writing one line to a protected path costs a precondition, a named loss and a rejected route, content-addressed and bound into history. Declining an entire issue costs thirteen characters and no articulation at all. The cheapest place to punt is the one place nothing prices it, and it is the place a punt does not need a row. It is gateable on exactly this PR's logic, without judging: report the set of keys the body DECLINES, and let the only exits be closing them or spending an admission — same The detector was the repository's owner, asking twice. Same as your measurement. Not implementing it here: this PR owns Generated by Claude Code |
The harness task list is the most legible statement an agent makes about what it believes is outstanding, it is displayed every turn, and nothing in the tree could see it. So a branch landed with half its list `pending` and every gate stayed green. That is this repository's recurring defect rather than a new one — a sensor with no ratchet on it. `stop_nudges` rule 5 asks the right question with no exit code; `ready.rs` emits `dialect prose` with nothing downstream; `graph-check` counts a `wip` in the wrong unit. Measured 2026-09-01: three items sat `pending` while the session reported the work as planned, and the only detector was a human asking. A VERB WRITES THE STORE, NOT A HOOK, and that is the design rather than a convenience. Recording from the harness's own todo tool needs a spelling per host — `TaskCreate`/`TaskUpdate` here, `write_todos` on Gemini CLI, `todowrite` on OpenCode, `update_plan` on Codex — and fails three different ways that are indistinguishable at the gate: an unsurveyed harness, a tool a setting switched off, and a compliant agent all record nothing. OpenCode makes it concrete by denying `todowrite` to subagents at session creation whatever the config says. `batten record plan` inverts the direction: the agent tells the engine, so a missing record REFUSES, identically everywhere, with no survey and no setting that can disarm it. Two arms, and the second is not optional. `plan-unfinished` refuses an entry left in flight. `plan-unrecorded` refuses a branch that recorded nothing at all — without it the first arm is satisfied completely by silence, which is the vacuity `mutate` already refuses by REPORTING a declared mutation whose named case does not exist rather than counting it. THE VACUITY ARM'S FIRST DRAFT WAS TOO WIDE, and the measurement is why it is keyed on the claim receipt now. Asking only for a non-empty diff is true of every scratch fixture and every consumer checkout: it reddened four `cli.rs` cases whose only business was exercising unrelated rules. A rule that fires on any dirty tree makes the committed config unusable over a test repository, and a rule like that gets switched off. `input.tree.records` already reads `.git/batten-receipts/<name>.<branch>`, so no new `Fact` was needed — but `recorder_records` read only the DECLARED stores, so a verb-written one was invisible whatever a module asked for. It now unions `record::VERB_WRITTEN`, unconditionally: the engine owns both ends, so there is no declaration for a consumer to forget and no unrelated table to make a gate's liveness depend on. Five gates caught halves I would otherwise have shipped: the leaf-verb dispatch census, the emitted row set, the mutation census, the pointer-only disposition, and the derived man/completions artifacts. 3773/3773 green. Refs: CLOUD-472 Admits: aba1e62b2c5357a2f1c428f8043c4360b5eb3c467e39a3b88b34237f6b474abb Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: 3c63c4e Admits-epoch: 04d65e929fcb8962b43da408ccb71d1f8d5a63c839a9492142bf7009f372e12a Admits-author: alec@wenzowski.com Admits-prev: e21549fcda550f97b7487fa035daa0f02606801a168f44f2f5b8ec7c0cc74feb Admits-answer-lost: The plan store has a writer and no reader, which is exactly the dead-gate class `crates/batten/src/record.rs` exists because of — its header records two landed readers with no writer, two `deny` rows deciding nothing. Here it would be the mirror: `batten record plan` writes a store no rule reads, so an agent could land with half its declared work in flight and every gate stays green, which is the defect measured on this very session. Admits-answer-precondition: A `[[rule]]` row and its `[[verdict]]` classes are only expressible in batten.toml — the file IS the registry, no verb writes either, and `policy/plan-complete.rego` cannot load at all until `V-PLAN-UNFINISHED` and `V-PLAN-UNRECORDED` are declared, because a module raising a token no row declares is refused at load. The path's own redirect says to change it in a pull request, which is what this is, and `mise run config-lint`, `mise run policy-test` and the compiled tier judge it before it binds. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a rule row and a verdict class; there is no verb that registers either. R-RESTORE-IT does not apply because nothing was destroyed — this adds one `[[rule]]` row and two `[[verdict]]` classes with their routes, and touches no existing row. The addition is strictly raise-only: two new deny classes, each with a declared override precondition, so it cannot weaken any gate. What a reviewer should check is the `plan-unrecorded` arm, because it refuses a branch that recorded nothing and therefore has the widest blast radius of anything in this change; it is gated on a non-empty diff and satisfied by an empty record, so the remedy is one call rather than a fabricated entry. Admits: 8137dd28e8352d32c3b5fd36e7a5f7152b979223890f2f11c5add2c1528c16ee Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: 3c63c4e Admits-epoch: 1893a9cd9dbd090116123001501b72aa60042d6906885362d4b43c0f6cfad073 Admits-author: alec@wenzowski.com Admits-prev: aba1e62b2c5357a2f1c428f8043c4360b5eb3c467e39a3b88b34237f6b474abb Admits-answer-lost: The rule row cannot load, and a module that fails to load takes its whole bundle with it. The plan store keeps a writer and no reader — the dead-gate class `record.rs` was written because of, in mirror image. Admits-answer-precondition: Second half of one change, split only because an admission covers one write. The `[[rule]]` row landed under the previous admission; `policy/plan-complete.rego` raises `V-PLAN-UNFINISHED` and `V-PLAN-UNRECORDED`, and a module raising a token no `[[verdict]]` row declares is refused at LOAD — so the tree between the two writes does not merely lack a feature, the module fails to load. batten.toml is the registry and no verb writes a verdict class. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a verdict class, and the path's redirect says to change it in a pull request. R-RESTORE-IT does not apply because nothing was destroyed — two classes and their routes are added, no existing row is touched, and the change is raise-only. The route to check in review is `R-OVERRIDE-PLAN-UNFINISHED`'s precondition: it must be answerable only for work that genuinely is not this branch's to finish, or the gate becomes payable in typing, which is the failure `V-FILED-UNREFINED` measured when a Ready block turned out to be the cheapest thing an agent can produce. Admits: 1e57c511055e0626046bd28c09c30389ad540aea48e85b0abecdf534385b9966 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: policy/plan-complete.rego Admits-head: 3c63c4e Admits-epoch: 02358ef15596a3d6bbc0781748ec5d6637cad09abc0c7e143f2eebeb2770950d Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: The arm stays too wide and the committed config becomes unusable over any scratch tree. Measured just now: `plan-unrecorded` keyed only on a non-empty diff reddened four `cli.rs` cases — `the_committed_delegating_rule_spawns_nothing_when_its_glob_misses`, `a_tracked_instruction_may_not_prescribe_the_denied_commit_identity`, `the_committed_portability_rules_fire_on_every_banned_shape` and one more — whose only business was exercising unrelated rules over a fixture repository. A rule that fires on every dirty tree is one that gets switched off. Admits-answer-precondition: A registered .rego module has no owning verb — the file IS the surface that declares the predicate, so narrowing `plan-unrecorded`'s precondition can only be done by writing it. The write lands in the reviewed PR for CLOUD-472, where `mise run policy-test`, the compiled tier in crates/batten/tests/it/plan_complete.rs and the declared `#MUTANT` rows judge it before it binds anything. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because no verb writes a predicate into a registered module; the module file is the owning surface and the path's redirect says to change it in a pull request. R-RESTORE-IT does not apply because nothing is being restored — this NARROWS a refusal I added minutes ago in this same branch, which is a tightening of the change rather than a reversal of anything landed. The property to check in review is that the new precondition is the claim receipt: it must select branches doing tracked work and nothing else, because keying it any wider is what the measurement above refutes and keying it narrower would let an unclaimed branch escape the gate entirely.
`unfinished-entry-unread` mutated `not done(entry.status)` while the arm binds `entry_row`, so the sed matched nothing and `mutate sweep` reported `inert-mutation` rather than counting it. INERT AND SURVIVED ARE DIFFERENT DEFECTS, which is why the runner keeps them apart: a survivor altered reachable code and its named case could not observe the change, while an inert row never altered anything at all. Read as coverage, both are the same lie — the census counts the module enforced while the sweep has proved only that a no-op leaves the suite green, which is CLOUD-418's finding reproduced inside the mechanism built to catch it. Sweep after: `plan-complete` reports neither row, and the totals are back to the 13 survivors and 1 could-not-look that stood on `main` before this branch. Refs: CLOUD-472 Admits: f67a29e4dfe3e0b673c8ee323b505f75b6723efc16306b55c9b55899715b7dfc Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: policy/plan-complete.rego Admits-head: dda2bdb Admits-epoch: 02358ef15596a3d6bbc0781748ec5d6637cad09abc0c7e143f2eebeb2770950d Admits-author: alec@wenzowski.com Admits-prev: 1e57c511055e0626046bd28c09c30389ad540aea48e85b0abecdf534385b9966 Admits-answer-lost: `plan-unfinished` ships with a declared mutation that does nothing, which is worse than declaring none: the census counts the module as enforced while the sweep proves only that a no-op leaves the suite green. That is exactly CLOUD-418's finding — a test that cannot discriminate shipping as coverage — reproduced inside the mechanism built to catch it. Admits-answer-precondition: A `#MUTANT` row lives in the module beside the predicate it mutates — there is no other surface that can carry it, and a registered module has no owning verb. The row is currently INERT: its expression names `entry.status` while the arm binds `entry_row.status`, so the sed matches nothing and `mutate sweep` reports it rather than counting it. Fixing it means writing the file. It lands in the reviewed PR for CLOUD-472 where the sweep itself is the check. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because no verb writes a `#MUTANT` row; the module is the surface. R-RESTORE-IT does not apply because nothing was destroyed — this corrects an expression added in this same branch so that it actually alters reachable code. What a reviewer should check is the sweep output rather than the diff: the row must move from `inert-mutation` to caught, because a mutation that applies and is still not observed by its named case is a different defect wearing the same word.
…d none `operation_of` is a static table from one survey (CLOUD-209), and its own comment records the trap: Gemini and Copilot carry no spellings because the survey did not record them, which is an absence of DATA that reads as an absence of CAPABILITY. I made exactly that inference earlier in this branch — treating a config gap as proof those hosts had no todo tool, and designing around an abstention that was never established. So the capability matrix gains a column, and it has two variants for a three-valued fact. `Surveyed(&[])` is a measured "this host offers none"; `Surveyed(&[..])` carries spellings that were FETCHED; `Unsurveyed(owner)` is nobody having looked, and is never reported as none. Fetched 2026-09-01 from vendor docs: Gemini CLI `write_todos` (on by default, disableable via `useWriteTodos`), Codex CLI `update_plan`, and this host's own `TaskCreate`/`TaskUpdate`. Cursor was SEARCHED and is still `Unsurveyed`: it has a Todos feature since 1.2, but the results were forum threads and third-party articles rather than a documented tool name, and a spelling taken from those is what CLOUD-209's rule refuses. BATTEN DOES NOT GATE ON THIS COLUMN. `plan-complete` reads a store `batten record plan` writes, so it fails closed on every host whatever the table says. What the column buys is the mirror — keeping the human's native todo view in step — and an honest report of hosts where that view does not exist. `doctor` reports it, and the check's shape is the interesting half. A check that reddened on every unsurveyed host would be permanently red here, and a diagnostic that never goes green stops being run. So an unsurveyed row must NAME the row that owes the survey, and naming one changes no exit code — `#MUTANT-OWNER`'s bargain one layer over, where the declaration buys that the gap is stated rather than that it is forgiven. What it catches is a harness added with neither a fetch nor an owner, which is the moment the gap goes invisible. 3775/3775 green. Refs: CLOUD-472
`REQUIRED_CLAIMS` has forced `tests` since CLOUD-453 and every entry has carried a `mutation` since CLOUD-418 — the obligation as a field, where an entry that cannot name the change which would kill it cannot be written. It was PROSE, so it was joinable to nothing. A slug is: `batten mutate` resolves it, applies the expression, runs the named case, and a SURVIVOR is the finding. That is the difference between "pressure tested" as an assertion and as an exit code. SHAPE HERE, RESOLUTION AT `verify`. The case does not exist at refinement time, so resolving the slug now would refuse every honest row before its code was written — the false-failure trap CLOUD-472's own §3 names. Whitespace is the whole discriminator, because `mutate`'s three-field row format already forbids it in a slug. The shared fixture carried the exact defect the check exists for — a sentence where a resolvable token belongs — so it is corrected at source and every case inherits the right shape. Both directions are asserted: prose refused, and the unmodified fixture clean, because a refusal whose remedy is unreachable is a wall. Also fixes a duplicated `#[test]` this branch introduced, which the test tier could not see: it is a warning, and only clippy's `-D warnings` promotes it. 3775/3775 green. Refs: CLOUD-472
|
❌ The last analysis has failed. |
Superseded by #811, which carries these eight commits replayed onto the same base, plus the verdict-grammar rename they need (
plan declare held,plan declare absent,issue file held— the three had landed here in the retiredV-SCREAMING-KEBABform andconfig lintrefuses them on the merged tree).Splitting one session's work across three branches is what this closes: CLOUD-1231 was implemented here and again on #818, independently, two hours apart. One PR now.