Skip to content

feat(policy): the punt sweep gets an exit code - #817

Closed
wenzowski wants to merge 8 commits into
mainfrom
claude/suite-subject-retirement-g2g00c
Closed

feat(policy): the punt sweep gets an exit code#817
wenzowski wants to merge 8 commits into
mainfrom
claude/suite-subject-retirement-g2g00c

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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 retired V-SCREAMING-KEBAB form and config lint refuses 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.

`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
@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
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

stop_nudges rule 5 already asks the right question at the end of every turn:

"Every row above was spun off while this branch was open. For each, by number: is it genuinely independent work, or a punt you could close here? Close the punts; leave a reason for the rest."

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 —

  • "the file is governed, so the fix is unlandable" — three times, over two different programs. .claude/rules/toolchain.md says the opposite: a defect in a governed program is a retirement row, not a block.
  • "the row is In Review, so someone owns it" — In Review means landed. The row was pullable.
  • *"a hook-minted receipt would be *CLOUD-820's shape" — CLOUD-820 is a receipt whose ABSENCE weakens a gate; this one fails closed, so the class is inverted.

None of the four was caught by a mechanism. Three of them were invisible to filed-over-own-diff because their §1 named files outside the branch's diff, which cites_only exempts by design. The fourth was caught, and only because it named the diff. The detector was the human, asking twice.

What actually held, and it is the whole design input

One refusal in that session could not be argued with: V-FILED-UNREFINED declares no override route. batten override request answered *"declares no *override route, so it cannot be overridden" — there was no field to write an excuse in, so no excuse was written. Every other gate that was gotten past was gotten past through a prose field, including one where the articulation itself said the question did not fit.

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 binds

CLOUD-514 shipped the record this depends on and explicitly ruled the remaining half out:

Not in this issue. Deciding whether a given spin-off was legitimate — the judgement the gate must never make.

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 Articulation::recomputes. So the shape CLOUD-514 lacked exists: deny over an object, with the only exit an explanation written into history forever. The gate decides over the set; the author pays for the classification; nothing scores prose.

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.

  • **Authority boundary (§1). **policy/filed-here.rego (a third predicate beside filed-unrefined and filed-over-own-diff), batten.toml (its [[verdict]] row, the admit route, and the recorder's liveness line below), the hook path where a config-load failure is currently swallowed, and a compiled-binary tier under crates/batten/tests/it/. No change to land or to the Stop path — rule 5 stays exactly as it is, and this is the gate it never had.
  • Computable predicate (§2), CORRECTED after pressure test. Over records already on disk: the rows this branch CREATED (board-writes, kind issue) minus the keys the PR body closes (pr-closes), minus rows recorded before the branch's base, **and requiring **cites_only — the row's §1 names nothing this branch touched. Non-empty and unadmitted ⇒ exit 2. No network, no tracker call, no similarity comparison, no score.
  • cites_only is a REQUIREMENT, not a dropped exemption, and that is the correction (§2). The first draft omitted it, which made this arm strictly wider than filed-over-own-diff — every row that one refuses, this one refused too. That breaks the module header's own invariant ("TWO REFUSALS, AND NEITHER SUBSUMES THE OTHER") and shows a reviewer two findings for one row. Requiring it partitions the two instead: proximity keeps this row names your open diff, and this arm takes this row names something else and you filed it anyway — which is where all three invisible punts in the measurement above actually sat.
  • THE RECORD NEEDS A LIVENESS SIGNAL, or the gate is vacuous (§2). Measured on this row itself: it was created at 19:59 and never recorded, because the installed engine could not load the authority (rule lock-complete: format is not valid for kind "policy" — a config key had landed ahead of the binary). The engine failed open, silently, and wrote nothing. An absent record is then indistinguishable from a branch that filed nothing, and the cheapest way past this gate becomes an engine that cannot start. The two existing arms can fail open on absence because they judge a row's PROPERTIES; an arm whose subject is the SET cannot. So the recorder must be able to say I ran and this branch filed nothing, distinct from there is no file — and the config-load failure that produced this must stop being silent at the recorder.
  • The gate never classifies (§2), which is what keeps rule 3 satisfied. It reports a set. The author closes the row, lets the body close it, or spends an admission whose articulation says why it is independent work. A spent admission suppresses the finding by CLOUD-1120's mechanism.
  • **Where it fires (§2). **batten check --rule filed-here, in verify, before a runner is spent — the same seam the existing two arms use. Not at Stop: CLOUD-97 and CLOUD-219 each ruled a deny out there independently, and Event::carries_a_verdict is the authority. Rule 5's nudge and this gate are the feedforward and the refusal for one question, which is the pattern the repo already runs for deferral-check.
  • **It fires before the record is destroyed (§2), and that ordering is load-bearing. **land deletes board-writes.<branch> at the merge (CLOUD-774's scoping), on the premise that every row the branch filed is "landed, closed by the body, or recorded in the override log". That premise is false today for exactly the rows this gate is about — two of the session's four were none of the three — but verify runs before the merge, so the set is intact when the gate reads it. After this lands the premise becomes true, because the only way past the gate is one of those three. The deletion needs no change and the governed land.sh is not touched.
  • Shown able to fail (§2). Red before: a branch that filed a row the PR body does not close. Green and staying green: a branch that filed nothing; a branch whose filed rows the body closes; a row recorded before the branch's base; and a row with a spent admission. Plus the anti-vacuity mirror — an admission for row A does not excuse row B — without which one articulation buys the whole set and the gate is off.
  • Deliberately not in scope (§2). Judging whether a spin-off was legitimate; that is still the judgement the gate must never make, and is still not made. Changing rule 5. Changing the recorder. Gating comments — sink 2 stays cheap and unpriced, per CLOUD-514.
  • **Effect (§3). **read.
  • Output and exit (§5). The one contract: 0 clean, 2 the verdict, 1 could-not-look. Pointer-only — the issue keys and nothing else. Never a title, never a body, never an articulation's prose.
  • **Commit / bump (§6). **feat(policy) — patch (0.0.x).
  • Test obligation (§7). Over the compiled binary beside crates/batten/tests/it/filed_here.rs's existing cases, with a #MUTANT row naming the case that discriminates, per CLOUD-1267's declared-suite mechanism.
  • Blockers (§8). None. relatedTo CLOUD-514 (which built the record and ruled this out under the older premise), CLOUD-1051 and CLOUD-1278 (the admission and its durable half — the mechanism that makes it landable now), CLOUD-1120 (a spent admission suppresses its finding, which this depends on), CLOUD-854 and CLOUD-774 (the two measured false-positive classes whose exemptions this reuses), CLOUD-326 (the chat-deferral half, which this does not cover).

Acceptance

  • A branch that filed a row and neither closed it nor spent an admission for it does not land.
  • An honest split-out is never refused — it costs one articulation, and that articulation is in the commit message where a reviewer reads it.
  • An admission for one row excuses no other.
  • Nothing anywhere scores prose or compares similarity.

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.

closing-key-check (CLOUD-674) is built for the multi-key case. Its refusal is "this PR closes some of the keys its commits served and strands the rest", and it enumerates them — so a PR closing three rows is an expected, supported shape.

The claim receipt is not. $GIT_DIR/batten-receipts/claim.<branch> is one file per branch holding one key:

CLOUD-1161
ready-lint pass
claimed-at 2026-08-30T17:44:25Z
base e340f796a07b648a3c0385c9f59b20d9dbd10594
branch claude/retire-shell-gates-t77iuk

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 #770

That PR closes CLOUD-1145, CLOUD-1161 and CLOUD-1218 — a shape closing-key-check accepts and, on the last lap, actively demanded a Closes line for each. Its receipt records CLOUD-1161 only.

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. **claim-needs-receipt only asks whether the branch carries a receipt, so the second and third rows ride the first one's. config-lint's weakens lookup reads ^weakens lines from the same file, so a Weakens clause groomed onto row two would be unreadable unless row two's claim were the one minted — which would then strand row one. The gap is silent in both directions.

Why this is not CLOUD-516's or CLOUD-733's

Both 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.

  • Authority boundary (§1). The claim receipt's format and the code that mints and reads it — batten claim check and config-lint's weakens lookup. The board discipline is untouched: this changes what the receipt can record, not when a row may be pulled.
  • Computable predicate (§2). A branch that has claimed n rows carries all n in its receipt, and a read for any one of them succeeds. Minting a second claim must be additive rather than replacing, and re-claiming the same key must stay idempotent. Every reader that parses the file today — the weakens scan, and whatever asserts a claim exists — must resolve per key rather than positionally.
  • Deliberately not in scope (§2). Whether a branch should serve several rows. closing-key-check already settled that it may; this row only makes the receipt able to say so.
  • **Effect (§3). **write, to $GIT_DIR only, as today. No new surface and nothing tracked.
  • Output & exit (§5). Unchanged and pointer-only: keys, a timestamp, a base. Never issue prose.
  • **Commit / bump (§6). **fix(claim) — patch until 0.1.0.
  • Test obligation (§7). Shown able to fail per CLOUD-418. Red today: claim row A, then row B on the same branch, and read A back — it is gone. Green and staying green: a single-row branch's receipt is byte-identical to what it is now (or migrates cleanly), re-claiming the same key twice does not duplicate it, and config-lint's weakens lookup finds a clause groomed onto the second key. Without that last one the fix records the keys and still cannot serve the reader that most needs them.
  • Blockers (§8). None.

Acceptance

  • A branch serving n rows carries n keys in its receipt, and each is independently readable.
  • config-lint resolves a weakens clause groomed onto any claimed key, not only the first.
  • A single-key branch is unaffected — the common case does not change shape.

Found while landing PR #770: the branch served three rows, could record a claim for one, and no gate noticed.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 35 minutes.

Check out review usage here.

View limit details

Limit 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.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: da4c0cee-f733-4a52-b8d4-983f1e17543a

📥 Commits

Reviewing files that changed from the base of the PR and between 310cb21 and 1e3e8df.

📒 Files selected for processing (5)
  • crates/batten/src/doctor.rs
  • crates/batten/src/hook.rs
  • crates/batten/src/ready.rs
  • crates/batten/tests/it/doctor.rs
  • crates/batten/tests/it/ready.rs
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 72903f53-5c33-4b5d-94f7-f785764d53cd

📥 Commits

Reviewing files that changed from the base of the PR and between 3c63c4e and 310cb21.

⛔ Files ignored due to path filters (1)
  • crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap is excluded by !**/*.snap
📒 Files selected for processing (16)
  • batten.toml
  • completions/batten.bash
  • completions/batten.fish
  • completions/batten.zsh
  • crates/batten/src/cli.rs
  • crates/batten/src/record.rs
  • crates/batten/src/rules.rs
  • crates/batten/src/spec.rs
  • crates/batten/src/surface.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/plan_complete.rs
  • crates/batten/tests/it/pointer_only.rs
  • man/batten-record-plan.1
  • man/batten-record.1
  • mise.toml
  • policy/plan-complete.rego

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The PR adds branch plan recording through record plan, engine loading for plan records, and plan-complete enforcement for unfinished or unrecorded plans. It adds an optional Ready prose-to-claims cutover timestamp based on createdAt, with configuration resolution and trust tracking. It adds the V-FILED-AND-LEFT-OPEN policy verdict and related tests. Claim receipts now preserve compatible issue IDs across claims and reset after base changes. Workflow and policy guidance updates branch, bundling, WIP, capability, and threshold rules.

Merge Risk: ⚪ Minimal · up to 310cb

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 Free

Your 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 @coderabbitai help to get the list of available commands.

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.

Copy link
Copy Markdown
Contributor Author

A fifth measurement for CLOUD-1311, from #811's session, and it names a route this arm does not reach.

filed-and-left-open reads the rows this branch put on the board. My worst punt put nothing on the board.

I served CLOUD-1213, did not close it, and declined it with DO-NOT-CLOSE — a row that predates the branch by weeks. That is invisible to all three arms by construction:

  • filed-and-left-open — the set is rows this branch created; I created none.
  • filed-over-own-diff — same, no row to measure proximity on.
  • closing-key-checkpasses, and correctly by its own contract. HOLD_MARKER is a bare token; it asserts every served key is disposed, and has no opinion on whether the disposition is honest.

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 cites_only-style partition against the two arms that read filed rows, so no key can earn two findings.

The detector was the repository's owner, asking twice. Same as your measurement.

Not implementing it here: this PR owns policy/filed-here.rego right now, and a fourth arm landing from #811 against the same module would be two authorities on one question and a guaranteed conflict. Recording it where it will be built instead.


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
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant