refactor(ci): retire bot-issue onto the engine, and close every row this session opened - #811
refactor(ci): retire bot-issue onto the engine, and close every row this session opened#811wenzowski wants to merge 29 commits into
Conversation
CLOUD-1295 Retire `bot-issue` — 486 lines and 6 subcommands, all GitHub API plus one local receipt, and its receipt route is the wall a carry branch cannot pass
Why Carved from CLOUD-1164 in CLOUD-1160's shape — a dedicated row per program, so the verdict and the ledger are recorded against the thing being retired rather than against an umbrella. The forcing occasion. CLOUD-1213's A third receipt arm could be bolted into What the program is, read rather than classified. 486 lines, 323 of bats, six subcommands — and it is entirely GitHub API plus one local receipt write. No tracker, no Linear, no MCP.
One real blocker, and it is in the engine rather than in the program. Refinement — Ready Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Not in this issue Extending Refs: CLOUD-1164, CLOUD-693, CLOUD-1213, CLOUD-741, CLOUD-908, CLOUD-418 CLOUD-1213 `sbom-check`'s licence table needs a human on every action pin move, and only a person can regenerate it
Why Found while fixing CLOUD-1207 and deliberately not repaired there, because
What #771 already settled, so this row is smaller than when it was filed The original body carried two concerns. The second — that the lane burned CI So what remains is only the first: should this class need a person at all? A correction to the measurement above (2026-08-31) This row said *"Measured on #676: 34 × * The options
Refinement — Ready Refinement gate: Definition of Ready & Done. This body carries only specializations.
{
"source_of_truth": "mise-tasks/sbom-actions.tsv",
"gate": { "task": "sbom-check", "exits": [0, 1] },
"commit_type": "ci",
"blockers": [],
"tests": [
{
"file": "tests/sbom-check.bats",
"mutation": "drop the per-pin unmapped loop, so a moved action pin reports mapped"
}
]
}REOPENED 2026-09-01. Closed against #788 with §2's Option 1 never built — Acceptance
CLOUD-1164 Retire the 66 remaining single-program units — 251.3s, one delta each, with the blocked/unblocked verdict recorded per program
UNSCHEDULABLE AS FILED — 66 units in one row, never claimed, and it is why six bundles were built around it (2026-08-31)The blocked-column work below is now largely correct. The row's SHAPE is what is left, and** Measured: ranking every suite whose subject IS a The missing deliverable is a PARTITION into claimable waves — each wave a size one PR can
|
| class in the table below | its precondition | status |
|---|---|---|
| forge/board (16 programs) | CLOUD-1154 | DONE — input.tree.forge exists |
| out-of-root (4 programs) | CLOUD-1167 | DONE — input.tree.external exists |
| third-party exec (10 programs) | CLOUD-1171 | CLEARS THE WRAPPERS, NOT ALL TEN — input.tree["tool-verdict"] exists; see the residue note |
| transcript content (2 programs) | CLOUD-1172 | DOES NOT CLEAR THIS CLASS — it landed a COUNT; CLOUD-1254 owns the remainder |
| engine state (1 program) | CLOUD-1203 | DONE — input.tree.state exists |
| stranded (2 programs) | CLOUD-1156 | DONE |
| clock / live process — membership is CLOUD-1170's table, not this row's | CLOUD-1170 | STILL BLOCKED — decided, not built; there is no instant key on the surface |
This row's own blockedBy (CLOUD-1154, CLOUD-1156) are therefore both satisfied. What is stale is the prose, and a**
**session reading it will decline ~30 programs it could take today.
One class is settled outright, because the schema names the program
unlanded-check is not merely unblocked. input.tree.state's own description reads:
"the finding pointer lines the engine's own store accumulated on it … these are the***
**lines unlanded-check already reads, so a successor reads what the program read
**rather than a re-derivation that could disagree." *CLOUD-1203 built the fact for this
program by name. Take it first.
Two classes need PER-PROGRAM reading before the verdict flips, not a class-level flip
The precondition landing is not the same as the program being expressible, and this
row's sibling CLOUD-1163 got three verdicts wrong in exactly that gap — by
classifying a program instead of reading it. So:
-
**out-of-root splits. **
mcp-attach-check:135names a DECLARED path under${HOME}
and is expressible viaexternaltoday.transcript-corpus-check:97reads
$HOME/.claude/projects, a DIRECTORY whose members are discovered at runtime —
that is CLOUD-1251's class and is still blocked, because[[rule.external]]
declares one path per row. Readserena-mcpanddoctor-checkbefore assigning
either. -
transcript content DOES NOT FLIP, and that is now settled rather than suspected. CLOUD-1172 landed
input.facts.extracted— "a declared extractor's COUNT over this session's transcript — an integer over typed events, never a byte of the session". Read atorigin/main0683ce53,finding-sink-checkis a per-turn join with a negated arm: prose matching apath:linecitation, joined against the same turn'stool_userecords, each classed against theissue-read-checkreceipt's fifth field. A count is not grouped by turn and carries no tool name, so it cannot express matched A and not B within the same group.board-payloads:157extractsget_issuepayloads, which is further still from a count.The recorded reason was "
input.call.transcriptis the path only" and that is wrong now — the reason is that the fact is a COUNT by design, and widening it is what rule 4 exists to refuse. CLOUD-1254 owns the remainder and decides per program: a declared extraction whose RESULT is decided over, or an exemption by name. Both programs stay blocked until it answers. CLOUD-1172's own §2 scoped these two retirements out in as many words, so its Done status was never a clearance for them.
The third-party-exec class, read per program — and a CORRECTION to the first attempt at this section
An earlier revision of this section, written 2026-08-31, named four programs as the class's "residue" and was wrong about two of them. It was written by classifying from a bucket rather than opening the files, which is the defect this row states two sections down — committed inside the correction for it. Each verdict below now quotes the program's own #MISE description or a path:line, so the next reader can check it without re-deriving.
input.tree["tool-verdict"] answers a wrapper around a tool whose verdict is keyed to (tool, pinned version, input digest): renovate-config-validator, pkl-check, hook-profile-check, macos-link-check, evaluator-io-check, mutant.
evaluator-closure-check is the class's BEST fit, not its residue. The earlier revision said it "wants a resolved dependency graph, not a tool's pass/fail — a verdict record cannot carry one." Read at origin/main 0683ce53:
:98—cargo metadata --locked --format-version 1. A third-party tool overCargo.lock, which is exactly the (tool, pinned version, input digest) keytool-verdictis built on.:83–:89—BATTEN_EVALUATOR_METADATAalready substitutes a file for cargo's output. The seam a recorded verdict needs is landed in the program.
So "a verdict record cannot carry one" asserts an impossibility that does not hold. The open question is narrower and real: whether the producer records the reachability FINDING (finding name -> pointer, which is tool-verdict's declared shape) or the graph itself — the decision at :104 is currently made in python over the whole metadata. Answer that; do not treat it as blocked.
perf-assert is not a producer and does not belong in this class at all. The earlier revision filed it under "producers, not gates". Its own header: "Gate: every measured invocation path is inside its latency budget, and README publishes the budget this gate enforces" and *"A PURE FUNCTION OF STDIN, the *graph-check/claim-check interface: agents fetch, gates decide." It carries a #MUTANT row at :54, exits 0 pass / 1 over budget or README disagreement / 2 could-not-look, and its §5 clause is "never the raw hyperfine output, never a command line". That is a board-gate-shaped predicate over a produced record plus README's budget column — input.tree.produced/records and input.tree.lines, all landed. Read it and place it; it is plausibly unblocked today.
perf-record and perf ARE producers, and this is why the other two slipped. Three of four in one bucket read as a pattern and the reading stopped. perf-record: "Append a measurement to the trunk's invocation-cost series in git notes … refuses to run off main". perf: *"Measure batten's invocation cost … THIS MEASURES; IT DOES NOT DECIDE. The verdict is *mise run perf-assert's." A producer has no verdict to key on; what it needs is a home for what it writes, which is a different question from expressibility. perf also has no suite (one ledger arm, per the four-programs note below).
Read each of the ten before assigning it, and quote what you read. The rule below is not new — this section is its fifth violation and the first where the violator was the correction itself.
CORRECTION (2026-08-31) — signing-posture was never in the clock class, and this row put it there
The blocked table listed signing-posture 2.1 under clock / live process. It decides over neither. Its own header: "Gate (and, with --repair, the write): no commit is signed by a key that cannot be verified or reproduced (CLOUD-669)" — a key-verification predicate. It is not in CLOUD-1170's member table, and CLOUD-1170 is the owner of that class.
So a program with no blocker here has been parked on CLOUD-1170 and is not in the ~30 unblocked list either — it fell between the two. Its real disposition is unrecorded: read it and place it, rather than moving it to "unblocked" on the strength of this correction alone, which would be the same mistake in the other direction.
The cause is restatement, and it propagated. CLOUD-1170 enumerates the class per program with a stated reason. This row restated three names and added signing-posture; CLOUD-1155 restated six and added land — correctly, since CLOUD-1170 had omitted it; CLOUD-1151 then merged this row's error with that table and published ten names, one wrong and one missing. Three copies, three mutations, the owner never consulted. That is CLOUD-1166's defect one type over — a membership LIST rather than a count — and the fix on every consumer row is the same: cite the owner, never restate it.
A text scan does not settle it either. git grep for date +%s or kill -0 reads false in both directions here: target-ensure delegates its lock to with-lock, hook-latency-drift times through hk, and step-receipt/graph-check merely WRITE a stamp. Membership is what the program decides over, which is the reading rule this row already states two sections down — and which the session writing this correction violated before it followed it (CLOUD-844's defect).
The rule the three wrong verdicts on CLOUD-1163 bought
Read the program before recording why it cannot move. Every wrong blocked verdict
found today came from taking the program's category as its predicate. The reading took
minutes in each case, and in each case the recorded reason would have sent an
implementer to build the wrong thing.
Blocked — REWRITTEN 2026-08-31 against the landed fact surface
The previous version of this table was written 2026-08-30 and listed six blocked classes.
Five of the six had their precondition land and the table was never edited, so it went
on declining ~30 programs a session could take. This is that table with each class's
current verdict, and the row's own blockedBy on CLOUD-1154 and CLOUD-1156
has been removed, both being satisfied.
| class | verdict today | what remains |
|---|---|---|
forge / board (16 programs: bot-issue, linear-check, land-divergence, ready-cites-check, done-check, sonar-gate, branch-age-check, timeout-drift, release-due, ci-drift, attestation-check, release-backfill, nonverdict-assert, land-divergence-assert, land-lock-check, ci-lease-precondition) |
UNBLOCKED | CLOUD-1154 Done — input.tree.forge is on the surface. Each still owes a per-program read before its verdict is written |
| out-of-root | SPLIT | a DECLARED path under a named root is input.tree.external (CLOUD-1167, Done) — mcp-attach-check:135 qualifies. A DISCOVERED SET does not: transcript-corpus-check:97 ($HOME/.claude/projects) and mcp-allow-check:327 (/tmp/mcp-config-cse_*.json) are CLOUD-1251, genuinely open |
| clock / live process | NOT BLOCKED — UNBUILT | CLOUD-1170 is DECIDED in writing, its only blockedBy (CLOUD-1177) is Done, and it is Urgent/Todo/unassigned. Blocked means nobody can start; this means somebody must. Read the membership there — never restate it |
| third-party exec | THE FACT EXISTS; THE PRODUCER DOES NOT | input.tree["tool-verdict"] landed (CLOUD-1171, Done), but nothing in the tree WRITES a record — crates/batten/src/tools.rs only reads .git/batten-tools/<tool>‖<version>‖<digest>, and the sole writer is crates/batten/tests/tool_verdict_facts.rs. So policy/validator-verdict-clean.rego resolves null and decides nothing today. The gap is a producer verb, not a fact |
transcript content (finding-sink-check, board-payloads) |
GENUINELY BLOCKED | CLOUD-1172 landed a COUNT, and a count cannot express a per-turn join with a negated arm. CLOUD-1254 owns the remainder |
engine state (unlanded-check) |
UNBLOCKED, and named in the schema | CLOUD-1203 Done; input.tree.state's own description names this program. Take it first |
| stranded | UNBLOCKED | CLOUD-1156 Done |
Three classes remain open, and only three: CLOUD-1251 (a discovered out-of-root
set), CLOUD-1254 (transcript content), and the tool-verdict producer. Everything else
on this row is a successor waiting to be written.
The seconds in the old table are also stale — they predate CLOUD-1198's build-once
change and are wrong in both directions by 2–4x. Read every figure from
bench/suites/RESULTS.md at HEAD, never from this body.
schema-check, config-deprecations, skill-check and reference-check retired in**
**PR #780 and are no longer members. They shelled out to batten itself, and folding that away WAS the port — the landed proof of the existing-verb home (CLOUD-1199).
Four programs have no suite
doctor-check, gh-preflight, payload-field, perf — one ledger arm each, not two. All four sit inside larger units except perf.
Refinement — Ready (66 deltas; write successors for the ~30 that are unblocked)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
-
Authority boundary (§1). Up to 66 PRs, one per unit. Each deletes its program and its suite and lands its successor in the home the section above assigns — preset, consumer module, an existing §2 verb, prior art, or a withdrawal. No new
crates/battenverb. With **one **// carried:arm per deleted path, or CLOUD-1080's withdrawal arm where no successor is owed, and its$MUTANT_GATESentry and#MUTANTrows re-homed.mise.toml,hk.pkland.github/workflows/**invoke these by task name and are ungoverned, so every repoint is free. -
Computable predicate (§2). Per unit, conserved unchanged. A port that also fixes a defect cannot be shown to have conserved anything, so a defect found mid-port is filed, not folded in.
-
Each PR states its axis verdict (§2). Landable and expressible are independent, and a unit that is landable but not expressible must say so rather than producing a module that loads and decides nothing — CLOUD-845's dead-gate class.
-
Deliberately not in scope (§2). The blocked classes' preconditions — each is owned elsewhere (CLOUD-1154, CLOUD-1156, CLOUD-1160). Merging units. Changing any predicate.
-
**Effect (§3). **
readfor all but the producing programs (perf-record,board-payloads,dist-adjacent), which keep their existing class. -
Output and exit (§5). Pointer-only per unit; exit follows the
0/1/2/3table, could-not-look is3. -
**Commit / bump (§6). **
refactor(ci)— no bump, per PR. Below0.1.0every release-worthy type collapses to a patch, butrefactoris not one: it releases nothing at any version. CLOUD-595's correction. -
Test obligation (§7). Over the compiled binary in
crates/batten/tests/; **no **.batsfile is added or edited. One arm per deleted path (CLOUD-908) — and one arm, not two, for the four programs with no suite. Shown able to fail per CLOUD-418 per unit, each with its anti-vacuity mirror. Mutated: every#MUTANTrow re-homed,mutant-censusgreen after each PR, never only at the end. -
**Blockers (§8). **
blockedByCLOUD-1154 and CLOUD-1156 for the members named above; the ~30 unblocked members are dispatchable now.relatedToCLOUD-1151 (the wave owner), CLOUD-908, CLOUD-418. -
Weakens (§8) — the
config-deprecationsmember only, and §1's "every repoint is free" is wrong for it. §1 namesmise.toml,hk.pkland.github/workflows/**as the invoking surfaces and calls them ungoverned. That holds for the other members; it is false forconfig-deprecations, which is invoked from a[[rule]]row inbatten.toml— aprotectedpath, judged byconfig-lintagainstorigin/main. Retiring the program forces that row'sglobandcheckonto the successor, andconfig-lintreads a changed rule predicate as a weakening.The gate is correct in its own terms: the row does stop firing on the path it named. What the change buys is strictly more coverage, not less — the shell gate ran only when its own file changed, and the successor (
crates/batten/tests/config_deprecations.rs) runs undertest:cargoon everyverifyand every CI run. The deprecation predicate itself does not move:batten config deprecationsdecides it before and after, and the baseline is still the newest release tag in version order.Weakens:
rule-predicate-changedatrule[no-key-leaves-the-schema-unannounced].checkWeakens:
rule-predicate-changedatrule[no-key-leaves-the-schema-unannounced].globRecorded honestly: this clause was groomed after the work started, on 2026-08-31, once
config-lintrefused PR refactor(ci): retire config-deprecations, reference-check, schema-check and skill-check #780.batten.toml:274-288records the same gate blocking the same smell for CLOUD-437, where the resolution was to back the change out instead. The alternative here — dropconfig-deprecationsfrom the bundle and land the other three — was offered and declined; the claim receipt carries--bypass-sequence, which says the refinement was self-served rather than pre-groomed.
Acceptance
- Every one of the 66 units is either retired or carries a recorded verdict naming its blocking class.
- Every retired unit names its home, read from CLOUD-1174's table rather than decided per PR, and no PR adds a
crates/battenverb. - No governed program in the tree is unaccounted for — this row plus CLOUD-1155, CLOUD-1159, CLOUD-1160, CLOUD-1161, CLOUD-1162, CLOUD-1163 and CLOUD-1145 covers all 132.
mutant-censusgreen after every PR.bench/suites/RESULTS.mdregenerates and the recovered seconds are reported against the 251.3s this row claims.
Units 14–83 of 83, minus those filed separately.
CLOUD-1297 A receipt rule's `checks` is a conjunction, so the claim gate cannot accept the alternative receipts `verify` already accepts
Found while retiring bot-issue onto the engine (CLOUD-1295).
The defect
Rule::checks on a receipt row is documented as "the receipts a row requires, all of which must be valid before the matched call is allowed" (crates/batten/src/rules.rs). There is no disjunctive spelling, so a row cannot say this receipt OR that one.
verify's shell body does express the disjunction: it asks receipt status claim, then bot, then (as of CLOUD-1295) carry, and any one passing is enough. The mediated gate cannot follow it. claim-needs-receipt demands claim alone, so an agent working on a branch that legitimately carries a bot.<branch> or carry.<branch> receipt is denied on every write despite holding a valid attestation.
That is a live false positive today for the bot receipt — pre-existing since CLOUD-693 — and CLOUD-1295 adds a third kind that inherits it. Nothing has broken, because nobody runs an agent session on a bot branch; the point is that the gate and verify disagree about what counts as a claim. CLOUD-741 is that same shape one layer in, and its finding was that the weaker of two readers is the one that gets consulted when the other is unloaded.
What was deliberately NOT done, and why
CLOUD-1295's plan called for a checks = ["carry"] row beside claim-needs-receipt. That row is wrong under the conjunction: a second receipt row is a second AND, so it would deny every ordinary write on every branch. The disjunction has to exist before the row can, so the row was not written and this was filed instead.
Refinement — Ready
- **Source of truth (§1). **
crates/batten/src/rules.rs—Rule::checksand the receipt kind's adjudication.mise.toml'sverifybody is the reference behaviour the gate should agree with, and is not this row's subject. - Computable predicate (§2). A
receiptrow can express "any one of these receipts is valid", and a mediated write on a branch carrying only abot.<branch>orcarry.<branch>receipt is allowed byclaim-needs-receipt, while a branch carrying none is still denied. Both directions driven over the compiled binary. - Effect (§3). No new verb and no new flag: a column on an existing rule kind.
- Output & exit (§5). Unchanged.
- **Commit / bump (§6). **
feat→ no bump. - Test obligation (§7). A compiled-binary case per direction, plus the vacuity case — a row declaring the disjunction must still deny a branch with no receipt at all, or the alternation is an allow wearing a gate's name.
- Blockers (§8). None. Adding a receipt to an alternation LOWERS a bar, so
config-lintwill read the consumer row that follows as a weakening and it owes a groomedWeakens:clause — work inside this row rather than a block from another.
CLOUD-1299 A frozen bats suite pins a remedy STRING, so retiring the program that string names has no landable shape
Measured while retiring bot-issue (CLOUD-1295).
The defect
tests/verify.bats asserts that verify's no-receipt refusal names its remedies:
[[ "$output" == *"bot-issue receipt"* ]]
That is a pin on PROSE, not on an invocation. Retiring mise-tasks/bot-issue.sh makes the remedy name a task that no longer exists, so the message has to change — and changing it fails that assertion, so the suite has to change too. tests/verify.bats is a .bats under tests/, which governed_at_head selects, so V-SHELL-RULE-EDITED refuses the edit with no override route and no bypass_env.
**And the repointing arm cannot admit it either. **repoints_at_the_declared_invocation (CLOUD-1219) requires the replaced span to satisfy is_retired_reference, whose arms are the repo-relative path, a $(dirname "$0")/<basename> sibling construction, or a variable bound to one of those. The span here is bot-issue receipt — a task NAME plus a subcommand, which is none of them.
The module already accepts that a caller can name a program by its stem: naming_forms yields {mise-tasks/bot-issue.sh, bot-issue.sh, bot-issue}, mentions_retired's last arm uses it, and drops_a_retired_name (CLOUD-1224) exists precisely for a list that names a program by stem. is_retired_reference is the one place that does not — and widening it to contains a naming form would be the licence the module's own comment refuses in as many words ("contains is honest here where on the addition side it would be a licence").
Why the two established escapes do not apply
- A
mise.tomlwrapper (CLOUD-1248, withdrawn on exactly that ground) keeps a retiring program's task NAME alive so callers are untouched. It works for an invocation. It does not work for a pin on prose: nothing about a wrapper makes the remedy sentence honest, and resurrecting a shell dispatcher is the thing the retirement removes. - **Retire the suite whole. **
tests/verify.bats' subject ismise.toml'sverifybody, notbot-issue.sh. Retiring it is a different unit's work and would be scope the retirement never claimed.
What CLOUD-1295 shipped instead, and why it is not a fix
The remedy now reads `batten claim bot` (which replaced `mise run bot-issue receipt`) — the live verb first, the old name kept as the thing it replaced. That is TRUE and is what a reader mid-transition needs, and it satisfies the assertion because the old substring survives. It is still a dead task name kept alive because a test pins it, and it will read as a slip to the next person unless this row exists. mise.toml carries a comment at the site pointing here.
The same shape is already recorded once, in mise.toml's own verify body, for CLOUD-1153: "a row whose §1 is written in the wrong shape — it needs the classification where a gate can already see it, not a fourth authority in this body — and it is left on the board rather than smuggled through a governed file." This is that class a second time, which is what makes it a row rather than an anecdote.
Refinement — Ready
- **Source of truth (§1). **
policy/shell-retirement.rego—is_retired_referenceand theadmitted_additionarms.tests/verify.bats' remedy assertions are the measured instance, and are not this row's subject. - Computable predicate (§2). A governed suite asserting a remedy string can have that string repointed at the retired program's declared successor, and the edit is admitted; an edit that changes anything else on the line is still refused. Both directions driven over the compiled binary, plus the vacuity case — an arm that admitted any span containing a naming form would be the licence this row must not become, so a case must show a same-line rewrite still refused.
- Effect (§3). No command-surface change: an arm on an existing policy module.
- Output & exit (§5). Unchanged — pointer-only,
path:lineand a rule id. - **Commit / bump (§6). **
refactor(ci)→ no bump;refactorreleases nothing at any version. - **Test obligation (§7). **
crates/batten/tests/over the compiled binary; no.batsadded or edited, whichV-SHELL-RULE-ADDEDrefuses anyway. The module's owntest_rules are the load-time tier. - Blockers (§8). None.
relatedToCLOUD-1219 (the invocation field this extends), CLOUD-1224 (naming_forms, whose stem this needs), CLOUD-1248 (the wrapper escape that does not cover prose), CLOUD-1295 (where it was measured).
CLOUD-1301 The reclaim verdict is reported once per SESSION where the fact is once per BOOT, so a container that replaced one mid-landing repeats it forever
OVERTAKEN IN PART (2026-09-01) — the hard block is gone, the repeat is not
main retired .claude/hooks/session-start.sh into ten declared handler rows (CLOUD-312 row 10, commit 7d188580) while this row was being written, and **took **tests/session-start.bats with it. The silence assertion that made this a landing blocker therefore no longer exists and verify is passable again — measured on CLOUD-1295's branch, which rebased onto that commit and dropped its own fix as subjectless. The title is rewritten to what is left; everything below is the original body and is kept for the measurement.
What remains is the noise half, unchanged. The reclaim report is now the task runner's own session:reclaim task, which records this boot and then runs reclaim-census report, each guarded so neither can fail the session. It still runs on every session start and still prints a verdict that cannot change while the container lives, so on a container that replaced one mid-landing the same true fact repeats every session forever. The committed authority's own note on the move classifies that output as Advise, "delivered on the same channel. Unchanged" — carried deliberately rather than overlooked.
§1 moves accordingly: the surface is that task body, not the deleted hook, and the task runner's manifest is ungoverned, so the fix is a plain edit rather than a retirement. §2's predicate is unchanged. What is LOST is the standing gate — the case that would have failed on a repeat is gone with the suite, so a fix now owes its own test rather than inheriting one, and §7 below is the shape it needs.
A fix was written against the old hook and driven both directions by hand — mark cleared → reports, second run → silent — before main deleted its subject. The shape that worked: a mark keyed to the boot time, sitting beside the census log rather than in it, both paths from the census's own log-path and boot accessors, and every failure path still reporting rather than going quiet.
Measured while landing CLOUD-1295, where it blocked the branch outright.
The defect
Two landed mechanisms contradict each other on a container that replaced one mid-landing:
- CLOUD-451's sensor.
.claude/hooks/session-start.sh:283-288runsreclaim-census reporton every session start and prints the verdict whenever it exits0— which is theactivereading, "a landing was in flight when this container replaced the last one". The comment is explicit that only the positive reading speaks. - CLOUD-891's case.
tests/session-start.bats:147— "the hook runs green on this checkout" — runs that same hook and asserts[ -z "$output" ], on the stated premise that "the steps are all already satisfied" and that silence is the pass.
On a container whose predecessor was reclaimed mid-landing, the sensor is RIGHT and the case FAILS. Measured on this container:
$ ./mise-tasks/reclaim-census.sh report ; echo $?
reclaim-census: A LANDING WAS IN FLIGHT when this container replaced the last one (last beat 1788262156 under boot 1788215603, now 1788263249)
0
It is permanent for the life of the container, and it blocks landing
report classifies the PREVIOUS boot, resolved as "the newest recorded boot that is not this one". That is immutable history: recording this boot does not move it, and no landing completed here can change what the last container was doing when it died. The verdict therefore repeats on every session start until the container is replaced.
test:bats runs in the hk hooks step, which verify runs, which land runs per lap — so verify fails on this one case forever, and the branch cannot land from this container at all. It failed three consecutive land laps on CLOUD-1295 with nothing else red.
It is invisible in CI, which is why it has survived: the case opens with real_install_or_skip, so it only runs where a real install exists — an agent's own container, never a fresh runner.
Why neither side is simply wrong
The sensor is reporting a true fact that somebody must act on, which is exactly what CLOUD-891 says stdout is for. The case is right that a hook with nothing to report should be silent. What is missing is that the sensor speaks once per SESSION where the fact is once per BOOT, so after the first read it is the noise CLOUD-891 removed — the hook's own comment already makes that argument for the negative readings ("a line every session start would be noise in the overwhelming case where nothing happened") and does not apply it to the positive one.
Why CLOUD-1295 could not fix it
tests/session-start.bats is a .bats under tests/, which governed_at_head selects, so V-SHELL-RULE-EDITED refuses an edit with no override route and no bypass_env; the two landable shapes are retire it whole or leave it alone, and its subject is the session-start hook rather than anything that change retires. mise-tasks/reclaim-census.sh is governed on the same terms. The remaining surface — the hook itself — is CLOUD-451's evidence collection, and rewriting how that sensor reports mid-landing is another row's work.
Refinement — Ready
- Source of truth (§1). The
session:reclaimtask body, which is where the report moved when the hook was retired. The census program's own verdict is correct and is not this row's subject; it is named by task rather than by path because citing a file is not claiming it. - Computable predicate (§2). On a container whose predecessor was reclaimed mid-landing, the reclaim verdict is reported at most once, and a second session start on the same boot is silent. A container whose predecessor stopped on purpose stays silent throughout, and the first report on a genuine reclaim is still made: a fix that simply stopped reporting would delete the instrument CLOUD-451 built.
- Effect (§3). No command-surface change.
- Output & exit (§5). Unchanged and pointer-only — a verdict and two epochs, never a plan or a prompt body.
- **Commit / bump (§6). **
fix→ patch. - Test obligation (§7). Both directions plus the vacuity case, over a store seeded to each of the three verdicts: reported once on a reclaim, silent on the repeat, silent throughout on an intentional stop. A case that only asserted silence would pass over a sensor that never reports at all, which is the instrument deleted rather than fixed.
- Blockers (§8). None.
relatedToCLOUD-451 (the sensor), CLOUD-891 (the silence rule), CLOUD-696 (the last correction to this census), CLOUD-1295 (where it was measured).
CLOUD-1224 `board-sweep.sh` builds its callee's filename from a loop variable, so no substitution arm can reach it — the retirement admission has three shapes and this caller matches none
Why
only_drops_a_retired_reference now admits three added-line shapes, and mise-tasks/board-sweep.sh matches none of them for the gates it drives.
board-sweep.sh:229-230 does not name its callee at all. It iterates gate names and builds each filename:
for gate in "${gates[@]}"; do
prog="$here/${gate}.sh"Every admission arm needs a span that is a reference to a path this delta deletes. Here the reference exists only at run time — the text on the line is ${gate}.sh, and gate is bound by the loop header, not by an assignment retired_path_vars or script_dir_vars can read off the base side. So:
truncates_a_retired_reference— no, nothing is shortened;repoints_at_the_declared_successor(CLOUD-1121) — no,contains(was, gone)is false, the line carries no repo-relative path;repoints_at_the_declared_invocation(CLOUD-1219) — no,is_retired_referencecannot resolve${gate}.shto any deleted path.
This is asserted rather than left to be discovered. crates/batten/tests/shell_retirement.rs carries the case as a REFUSAL — the shape is spelled out and the module is pinned refusing it — because a caller the gate silently fails to match is indistinguishable from a caller it correctly admits, which is the class .claude/rules/policy-modules.md opens on.
What it costs, concretely
board-sweep.sh drives the board gates by name. Retiring any of them forces an edit here, and that edit is V-SHELL-RULE-EDITED with one route (R-PORT-AND-RETIRE), no override and no bypass_env. So the board-gate bundle is not landable one gate at a time while this stands — which is the same structural block CLOUD-1149 measured for the sibling spelling and CLOUD-1051 for the declaration table, one shape further on.
CLOUD-1221 retired ready-lint.sh and did not hit this, which is worth stating because it bounds the row: board-sweep.sh reaches ready-lint transitively through graph-check, never by building its name. The loop at :229-230 is over the gates board-sweep invokes directly.
Two shapes could close it, and they are not equivalent
A token-removal arm. Admit an added line whose only difference from a removed one is that a token this delta retires has been dropped from a list — removing a name from it is the honest edit when that gate no longer exists. Narrow, and it never introduces a byte.
(Correction, while building this: the list is not a gates=(...) array, as the Why above paraphrases it. board-sweep.sh:229 names the gates inline in the loop header — for gate in graph-check duplicate-close-check released in-progress-drain done-pr-check spec-ref-check; do. The shape of the fix is unchanged; the fixture had to match the real line rather than the paraphrase.)
A loop normalisation. Change board-sweep.sh so each gate is invoked on its own line naming its own path — the shape hooks-wiring-check.sh was normalised into for exactly this reason (policy/shell-retirement.rego's own comment: "with each row on its own line every row is independently removable, and the next retirement in this campaign does not have to shorten anything at all"). That makes the file permanently retirement-friendly, but the normalisation is itself an edit to a governed program and has to land through an admission that does not yet exist — so it cannot bootstrap itself.
The first is the landable one and the second is what it unlocks. Decide on the row; do not take the second as a given.
Refinement — Ready (admit dropping a retired name from a list)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- **Authority boundary (§1). **
policy/shell-retirement.rego'sadmitted_additiongains a fourth disjunct;batten.tomlgains any[[pattern]]row it needs. No governed program is edited by this row —board-sweep.shis the consumer whose future edit this makes landable, and touching it here would need the very admission being added. - Computable predicate (§2). An added line is admitted when it equals a removed line with one whitespace-delimited token deleted, and that token names a path this delta deletes — matched through forms computed from
gonerather than by string similarity. (Built:naming_formsyields the path, its basename, and its basename without the extension. The third is the one this needs, because the loop names TASKS and the files carry.sh— CLOUD-865's split — so the token isgraph-check, not any filename.) - Both halves had to move, which §1 did not anticipate. The addition arm alone is not enough:
only_drops_a_retired_referencealso requires every REMOVED line tomentions_retired, andfor gate in old-gate other-gatenames no path by any arm, so the removal half refused it independently.mentions_retiredtherefore gains the bare-name form too. That arm is looser than its siblings — acontainsover a name — and it is safe for the reason the clause's own comment gives for the others: it decides a line that is GOING AWAY. What is introduced is checked by the addition arm, which requires byte equality with that line minus exactly that name. Neither half is tight; the pair is. The narrowings that make the third disjunct safe apply unchanged: the dropped span is DERIVED from the two lines rather than declared, and it must reference a deleted path, so an unrelated token cannot be smuggled out of a line. - Deliberately not in scope (§2). Normalising
board-sweep.sh's loop (the second shape above). Admitting an added line that drops a token naming nothing retired — that is an ordinary edit and must stay refused. Widening tomise.tomlor any ungoverned file, which needs no admission. - **Effect (§3). **
read. A policy module decides over committed text. - Output & exit (§5). Unchanged:
{rule, verdict, subjects}with a[[verdict]]row, pointer-only subjects, the crate's one0/1/2/3table. No new verdict token unless the refusal reason is genuinely new — reuseV-SHELL-RULE-EDITED, since this widens what that class admits rather than adding a class. - **Commit / bump (§6). **
feat(policy)— patch until0.1.0. It adds a declarable shape rather than fixing a wrong verdict, which is why it is notfix. - Test obligation (§7). Both tiers, per
.claude/rules/policy-modules.md:test_rules in the module, and cases incrates/batten/tests/shell_retirement.rsover the compiled binary — the tier that proves the ENGINE builds the input, which is where both live instances of the dead-gate class were caught. The discriminator is the pair:board-sweep.sh:229-230's real shape must go from REFUSED to admitted, and a line dropping a token that names nothing this delta deletes must stay refused. The existing refusal case is rewritten rather than deleted, so the change of verdict is visible in the diff. Shown able to fail per CLOUD-418. - Mutation (§7). A declared
#MUTANTthat discriminates — not one another conjunct already excludes, which is the survival CLOUD-1149's own row measured. Note thatshell-retirementis not in$MUTANT_GATES(mise.toml:457), so a declared mutation there is documentation until that is fixed; say so rather than claiming a red run. - Blockers (§8). None.
relatedToCLOUD-1149 and CLOUD-1219 (the two arms this extends, and the module this edits), CLOUD-1221 (the retirement that exercised them and found this gap still open), CLOUD-1051 (the truncation arm and the same class one shape down), CLOUD-1121 (the path substitution).
Acceptance
board-sweep.sh'sgates=(...)line, with one retired name removed, is admitted; the same line with an unrelated token removed is refused.- The existing refusal case in
crates/batten/tests/shell_retirement.rsis rewritten to the new verdict, not deleted. - Both tiers green; the declared mutation discriminates.
- No governed program is edited by this change.
Found while landing CLOUD-1149/CLOUD-1219: the admission was widened twice and this caller still matched nothing, which was recorded in a PR body and nowhere the board could see.
CLOUD-1142 An issue key gets one definition, in the fact producer — the three axes decided, three-valued, and a gate that fails the twenty-first copy
Why
CLOUD-761 measured twenty independent derivations of the issue-key regex across nine spellings, diverged on three axes — case, boundary, and whether the project prefix is required at all — with a shipped defect behind them: a PR body writing closes cloud-757 is accepted by closing-key-check and then invisible to claimed-keys and released. Two gates disagree today about what an issue key is.
That row decided all three axes and then named the whole remedy as one piece of work: one definition, plus nineteen consumer conversions. The two halves have very different shapes, and only one of them is landable.
**Seventeen of the nineteen consumer sites live in governed **mise-tasks/ programs — ready-lint.sh, graph-check.sh, closing-key-check and released among them. Converting a consumer in place raises shell edit refused, which declares one route, rule read first, with no override and no bypass_env. So the conversion half stays on CLOUD-761 with that blocker named, and this row is the definition half, split out so it can ship.
Withdrawn, 2026-09-01: there is no "cannot retire" set. This paragraph asserted that three of the seventeen "cannot retire at all, being resolved by path from siblings that would themselves need an added line to be repointed." That is false in both of its halves. The reasoning cites only_drops_a_retired_reference, which is the REMOVAL clause, to conclude about ADDITIONS — those are governed by admitted_addition, whose arms already admit a path repointing at a declared successor and an invocation repointing at a declared command, which is exactly the added line the claim says is unpayable. One of the three named programs, hooks-wiring-check.sh, is no longer tracked on origin/main at all: it was already retired while the claim stood. The same withdrawal is recorded on CLOUD-761.
Nothing here is a new decision. Every axis this row builds, CLOUD-761 already decided, and its wording is carried rather than re-argued.
Refinement — Ready
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Authority boundary (§1). One definition of an issue key, in the fact producer under
crates/batten/src/, declared as a typed board fact.batten.tomldeclares the identity, applicability, scope and severity of the anti-duplication gate over it; the generated schemas are derived bymise run schemaand byte-compared, never hand-written. **Nothing under **mise-tasks/**and no **tests/**/*.batsis added, edited or deleted here — this row converts no consumer, which is exactly what makes it separable from the half that cannot land. - Computable predicate (§2). The fact answers the key question once, on the three axes CLOUD-761 decided. Case: sensitive —
CLOUD-757is a key andcloud-757is not one, and a body writing the lowercase form is refused rather than normalised, because normalising up is what produced the shipped defect. Boundary: the explicit character-class form(^|[^0-9A-Za-z-])…([^0-9]|$), not\b, so the rule does not depend on how a given matcher treats-. Project prefix: mandatory — a glob cannot anchor, which is why[A-Z]*-[0-9]*acceptsAB-1,Z-9andA-1foo, andCLOUD-[0-9]*acceptsCLOUD-1x. The fact is three-valued: an unreachable tracker resolves to "could not look", never to an empty set that reads as a coherent board. The anti-duplication half is a gate over the tree: a second, independent derivation of the key pattern is a finding, because without one the twenty-first copy reappears and nothing notices, which is how twenty arrived. - Effect (§3).
read. Board state is bounded and cacheable, resolved atverify/config linttime through the existing boundary; no verb changes class and no per-checker spawn is added. - Output & exit (§5). Pointer-only: a refusal names the key and the site, never a body. The crate's one
0/1/2/3table with no per-verb exception — a malformed key is a policy verdict at2, an unreachable tracker is3, and1is a usage error. The2/3split must not collapse: that collapse is the vacuous pass this row exists to close. - Commit / bump (§6).
feat(facts)— patch until0.1.0, since below that release-plz bumps the patch whatever the type says. Not!for the consumer surface: no exit code or output shape a consumer sees moves, and nothing that is accepted today by the sites this row actually touches stops being accepted, because it touches no existing site.mise run semverdecides the library half, which is where a new fact type can move apubsurface without anyone meaning it to. - Test obligation (§7).
crates/batten/tests/*.rsover the compiled binary, each case shown able to fail. The fixed example set is the discriminator and it is written down rather than left to the implementer:CLOUD-757accepted;cloud-757,AB-1,Z-9,A-1fooandCLOUD-1xrefused; andCLOUD-17not matched insideCLOUD-179. The vacuity case, which a two-valued fact passes silently: an unreachable tracker must be distinguishable from a board with no relations. And the anti-duplication case, without which the whole row decays: a twenty-first independent derivation of the key pattern must fail the gate. Before that gate's severity is set to deny, replay the predicate overgit rev-list origin/mainand record commits examined, the would-fire count, and how many of those firings were false positives; the number is reported, never graded. - Blockers (§8). None on this half — the fact model's cost class and its three-valued contract, which CLOUD-761 waited on, have both shipped.
relatedToCLOUD-761 (the consumer half this was split from, which this row unblocks), CLOUD-757 (the fact model), CLOUD-251 (the vacuous-pass trap), CLOUD-773 (why an exit code cannot carry what the producer already knew) and CLOUD-418.
Acceptance
- An issue key has exactly one definition in the tree, and a second derivation fails a gate rather than passing unnoticed.
- All three axes are resolved by the stated behaviour rather than by whichever copy happened to run: case-sensitive, the explicit character-class boundary, project prefix mandatory.
- The fact is three-valued — an unreachable tracker yields "could not look", never an empty set.
- No governed shell program and no bats suite is touched, so the row lands without reaching the conversion half.
Not in scope
Converting the nineteen existing derivation sites, and issue-read-check:91's message-versus-glob disagreement along with them. That is CLOUD-761's remaining half and it is blocked on the shell retirement, not on this row. Also out of scope: the Ready-block clause structure beyond §8's relation, which CLOUD-761 already scopes honestly as prose that stays prose.
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 checkandconfig-lint'sweakenslookup. 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
weakensscan, 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-checkalready settled that it may; this row only makes the receipt able to say so. - **Effect (§3). **
write, to$GIT_DIRonly, 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'sweakenslookup 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-lintresolves aweakensclause 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.
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.mdsays 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 besidefiled-unrefinedandfiled-over-own-diff),batten.toml(its[[verdict]]row, theadmitroute, and the recorder's liveness line below), the hook path where a config-load failure is currently swallowed, and a compiled-binary tier undercrates/batten/tests/it/. No change tolandor 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, kindissue) 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_onlyis a REQUIREMENT, not a dropped exemption, and that is the correction (§2). The first draft omitted it, which made this arm strictly wider thanfiled-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:formatis 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, inverify, 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, andEvent::carries_a_verdictis 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 fordeferral-check. - **It fires before the record is destroyed (§2), and that ordering is load-bearing. **
landdeletesboard-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 — butverifyruns 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 governedland.shis 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:
0clean,2the verdict,1could-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#MUTANTrow naming the case that discriminates, per CLOUD-1267's declared-suite mechanism. - Blockers (§8). None.
relatedToCLOUD-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-472 A §7 test obligation can land with no test behind it: the Ready block is checked for shape, the code for correctness, and the two are never compared
Why
ready-lint gates the shape of a Ready block. verify gates the code. Nothing compares them, so a §7 obligation can be written into an issue, pass the refinement gate, and land with no test behind it — and every gate in the loop stays green while it happens.
Measured on this repo's own work, 2026-08-12, on CLOUD-369. Its Ready block carried:
(b1) with a green holder and several waiters, exactly one waiter readies and pushes and the rest stay in draft, plus the negative — a holder whose CI is not green admits nobody
The first half shipped and is tested. The second half was never implemented and never tested. The landed land admits a successor the moment it loses the lease, with no reading of the holder's CI at all — so the acceptance bullet describing when a second matrix may be bought does not describe the code that buys it. #369 merged to main by fast-forward, CI-confirmed green, with that divergence in it.
The failure is structural, not inattention. Reconstructing it:
- The acceptance was written first, and correctly.
- A collision with CLOUD-420 forced a mid-implementation redesign of the mechanism.
- The redesign was derived from the problem again, not from the acceptance — and silently dropped one condition.
- The tests were written from the implementation, so they assert what the code does. A test for the dropped condition was never written, because the code it would test was never written.
- Nothing anywhere reads the §7 clause and asks whether a test names it.
Step 4 is the load-bearing one. Tests written from the implementation can only ever confirm it; they are structurally incapable of catching an obligation that was dropped, because a missing behaviour has no code to write a test against. The Ready block is the only artifact that still remembers what was promised, and nothing reads it at implementation time.
The four open questions are ANSWERED, 2026-09-01 — and the first answer was already in the tree
This row sat in Backlog because its four questions were unanswered design decisions. Answering them turned up what the questions did not anticipate: three of the four already have a landed mechanism. The reason they are not working is not that the mechanism is missing — it is that it is OPTIONAL.
1. The binding token — CLOUD-453's claims object already is one
A Ready block may be written in either of two dialects. The prose dialect is validated for the clauses that are present. The object dialect is a fenced json block, and REQUIRED_CLAIMS in crates/batten/src/ready.rs demands source_of_truth, gate, commit_type, blockers and tests — each present and non-empty. check_claimed_tests then demands file **and **mutation on every entry, which is CLOUD-418's obligation as a field: an entry that cannot name the mutation which would kill it cannot be written at all.
That is the third form question 1 asked for, and it landed. The module's own comment says why it works: a key cannot be well-formed prose.
**MEASURED, IT IS UNUSED. **check_claims returns false when there is no fence, and the caller falls back to the prose path. Sampled 2026-09-01 over the 50-row Todo queue: CLOUD-1306, filed the same day, carries no object, and its §7 reads "Over the compiled binary or the successor's own tier. Shown able to fail per CLOUD-418, three discriminating observations: …" — three obligations, none joinable to anything. CLOUD-1311's own Ready block, written the same day, is the same shape and linted green.
So the defect is not a missing mechanism. The escape is that the prose dialect stands as an ALTERNATIVE rather than as a legacy.
ready.rs already anticipates this and already emits the measurement: it pushes dialect json or dialect prose per run, with the comment "A prose-only block still PASSES — every issue Ready today stays Ready, which is what lets the corpus converge deliberately instead of in one sweep." The convergence was left to intent, and intent did not converge it. The emission is the sensor; what is missing is the ratchet.
2. Where it runs — committed, and the declared set reaches the tree through the recorder
Decided by a property measured while landing CLOUD-1302: the filed-here record store lives under $GIT_DIR, is never committed, and dies with the container. A pull-time-only gate cannot be re-asked, and this row's whole defect is one found after the merge.
tests[].file entries are paths in the tree, so half the join is already about it. The DECLARED set reaches the tree through a surface that exists and is forgery-resistant: board-issue-groomed's verdict column lints the description the tracker returned, never a payload the caller assembles — earned, since ready-lint over a self-assembled payload was measured green three times against text in a local file, once under an id no row carried.
3. Ready AND land — two predicates, not one placement
The body treated this as either/or. It is neither.
- At refinement,
ready-lintasserts the obligations are declared and well-formed. Nothing is implemented yet, so nothing about coverage is asserted and there is no false failure. - At
verify, a tree gate asserts each declaredfileexists and each declaredmutationis caught. Late, but re-askable forever.
4. Deferred-but-covered — the [rule.conserves] ledger already spells it
// carried:, // subsumed:, // changed:, // withdrawn:, // ported:, each resolving its target by exact tracked path, with CASE_TARGET_MISSING when the named successor dies. CLOUD-369 §7(c), satisfied by another PR, is a subsumed arm. An obligation with neither a bound case nor an arm is the refusal, and that is the whole predicate.
Subagents are a producer, never the judge
Non-negotiable rule 3 forbids a gate resolving to a model verdict, so "spawn a reviewer and trust its finding" is unavailable. The role that is not that: a subagent PROPOSES a candidate mutation for an obligation carrying a file and no discriminating mutation, and mise run mutant decides — apply the expression, run the named case, it must go red; a survivor is the finding. Measured value this session, twice on one file: a declared mutation on privileged-lane survived because another conjunct already excluded its input, and a shell-retirement row was malformed because a | inside a Rego comprehension parsed as the field separator. Neither was findable by reading.
Refinement — Ready (increment 1 of 2: close the escape, then bind the obligations)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- **Authority boundary (§1). **
crates/batten/src/ready.rs(the dialect decision incheck_claimsand its caller), the consumer-declared cutover inbatten.toml, and a compiled-binary tier incrates/batten/tests/it/ready.rs. No change to the claims grammar, toREQUIRED_CLAIMS, or tocheck_claimed_tests— those already say the right thing and are simply unreachable. - Computable predicate (§2). A Ready block carrying no claims object is a finding when the row was created at or after a consumer-declared date. Before that date the prose dialect passes exactly as today. The comparison is the payload's own
createdAtagainst a fixed-width ISO-8601 literal, so it resolves with no network call and no second read of the tracker. - A RATCHET, NOT A FLIP, AND THE MEASUREMENT IS WHY (§2). Flipping it unconditionally refuses the entire Todo queue in one step — 50 rows, and the sample carries no object at all — while
graph-checkenforcesTodo ⇒ ready-lint exits 0, so the board's whole ready frontier would go dark at once. That is CLOUD-858's shape, where three rows madegraph-checkrefuse the whole board. Grandfathering by creation date closes the escape going forward and costs the standing queue nothing. - Could-not-look is not a refusal (§2). A payload with no readable
createdAtleaves the row judged exactly as today. A verdict about a missing field is not a verdict about the row, which is the three-valued posture the rest of this engine already takes. - The date is the CONSUMER's (§2). It is a fact about this repository's own board, so it lives in
batten.tomland never incrates/batten— non-negotiable rule 1, the same reason the tracker's vocabulary is not the crate's to name. - Deliberately not in scope (§2). Binding a declared
tests[].fileto a case in the tree and holding a declaredmutationto the sweep — that is increment 2, and it is the half this row was originally filed for. Changing whatREQUIRED_CLAIMSdemands. Migrating any existing row's body. Retiring the prose path outright. - **Effect (§3). **
read. - Output and exit (§5). The one contract:
0clean,1the finding,2could-not-look. Pointer-only — the block's line and the rule id, never a clause's text. - **Commit / bump (§6). **
feat(ready)— patch (0.0.x). - Test obligation (§7). Over the compiled binary in
crates/batten/tests/it/ready.rs. Shown able to fail, four discriminating observations: a post-cutover row with no object is refused; a post-cutover row WITH an object is clean; the anti-vacuity mirror — a pre-cutover row with no object is clean, without which the arm is satisfied by a check that refuses every prose block; and an unreadablecreatedAtleaves the row judged as before. - Blockers (§8). None.
relatedToCLOUD-453 (which built the claims object this makes reachable), CLOUD-418 (themutationfield), CLOUD-1311 (the sibling escape — a nudge with no exit code, where this is a mechanism with no reachability), CLOUD-858 (the measured cost of refusing a whole board at once), CLOUD-369 (the instance).
Acceptance
- A row filed after the cutover cannot reach the ready queue with a prose §7.
- The 50 rows standing in Todo today are unaffected, and
graph-checkstill passes over the board. - An unreadable creation date is a pass, not a refusal.
- Nothing scores prose, and no clause is judged for what it says.
Filed from the losing side: the divergence above was found by re-reading the issue after the merge, which is not a mechanism. The instance was fixed under CLOUD-369; this is the gate that would have caught it.
CLOUD-761 Nineteen issue-key consumers still re-derive the key, and every one of them is a governed shell program the conversion cannot touch
Lifecycle bundle integration
Extend canonical board facts with status, lifecycle revision, semantic body digest, structured relations, claim generation, bundle roster, and PR/main/release evidence. An unreachable tracker must yield explicit unknown facts, never an empty clean payload.
The largest amortisation win in the tree, and the one with a shipped defect already attached.
Board state — issue status, relations, Ready-block clauses, issue keys — is re-extracted from markdown by every checker that needs it. Measured:
| Signal | Count |
|---|---|
| Independently re-derived copies of the issue-key regex | 20 sites, 9 spellings — diverged on three axes, see below |
ready-lint: markdown-dialect extraction → predicate |
~95 lines → ~23 lines |
claim-race-check: fetching → predicate |
~50 lines → ~6 lines |
graph-check: total → genuinely relational part |
~700 lines → under 60 |
Those ratios have been read before as "an engine would only improve the small half". That reading is backwards: they are the price of nothing amortising extraction, paid once per checker, forever. Twenty derivations of one regex is not evidence that regexes are expensive — it is evidence that no fact exists.
And they have already diverged — on three axes, not one. Re-counted 2026-08-20 over code lines only (mise-tasks is 43–53% comment, so a naive grep inflates). The original "nine copies, diverged in case-sensitivity" undercounted the sites and missed the worst axis entirely.
Axis 1 — case. Five sites match case-insensitively and normalise up (closing-key-check:68, :98, claimed-keys:59, bot-issue:357, ready-lint:360); nine match case-sensitively (released:97, graph-check:216, :291, :302, ready-lint:314, bot-issue:295, :379, :436, deferral-check:104). Two expressions are mixed within two lines of themselves: bot-issue:357 matches insensitively and :358 re-extracts sensitively.
Axis 2 — boundary. (^|[^0-9A-Za-z-])…([^0-9]|$) (landed-check:63, done-check:108, both commented "so CLOUD-17 does not match CLOUD-179") versus \b…\b (board-move-guard:142, different semantics — - is a word boundary) versus grep -qxF (claim-race-check:127) versus unanchored.
Axis 3 — whether the project prefix is required at all. This was not in the original count, and it is the worst of the three. Four sites are shell case globs, not regexes: board-move-guard:104, issue-read-guard:71 and issue-read-check:88 all test [A-Z]*-[0-9]*; hooks-wiring-check:202 tests CLOUD-[0-9]*. Verified behaviour of [A-Z]*-[0-9]*:
| input | verdict |
|---|---|
CLOUD-757 |
accept |
AB-1 |
accept |
Z-9 |
accept |
A-1foo |
accept |
cloud-757 |
reject |
And issue-read-check:91 prints "is not an issue key (want CLOUD-123)" — an error message asserting a contract its own glob does not enforce. hooks-wiring-check:202 accepts CLOUD-1x for the same reason: a glob, not an anchored pattern.
The shipped defect is concrete. A PR body writing closes cloud-757 is accepted by closing-key-check and then invisible to claimed-keys and released. Two gates disagree today about what an issue key is. That is not a risk.
Cost class: read
Board state arrives over the tracker API — bounded, cacheable, resolvable at verify/config lint time. No 100ms budget question, and no §5 spawn question if resolved through the existing boundary rather than per-checker.
What this issue lands
- One authority for an issue key, replacing twenty derivation sites. This alone justifies the issue.
- Typed board facts: status, relations (
blockedBy/blocks/relatedTo), milestone membership, and the Ready-block clause structure — as data, not as a markdown substring hunted per checker. - The extraction paid once.
ready-lint's ~95 lines become a fact producer with one consumer today and several tomorrow; its ~23 lines of predicate stay predicates.
A caution this must respect. CLOUD-623's survey classified invariants by input shape and warned that where the hard part is turning prose into structure, an engine over already-normalized facts cannot help. That warning is right about engines and is exactly why this is a fact issue: the point is to do the extraction once and well, not to move the predicate to a different language. The Ready-block clause structure is the hard case — it is markdown with a dialect — and it should be scoped honestly: some clauses are structural (§8's blockedBy cross-check is already a relation), others are prose and stay prose.
A trap already recorded, not to be rebuilt. CLOUD-251 — "a rule set with no relations still reports the board coherent" — is the vacuous-pass failure. A board fact that resolves to "no relations" must be distinguishable from "could not look", which is CLOUD-757's three-valued contract doing real work here.
Acceptance
- An issue key has exactly one definition; the other nineteen sites consume it. Asserted by a gate, or the twenty-first copy reappears.
- All three divergences are resolved deliberately — one behaviour chosen and stated for case, for boundary, and for whether the project prefix is mandatory — not whichever copy happened to run. The four glob sites are the ones that change behaviour, so they need naming individually.
issue-read-check:91's message and its glob agree, in whichever direction is chosen.- Board facts are three-valued: an unreachable tracker yields "could not look", never an empty set that reads as coherent (CLOUD-251's trap).
- At least one existing checker consumes the fact rather than re-extracting, with its extraction half deleted — the proof that amortisation is real rather than asserted.
Two findings filed 2026-08-20 point here. CLOUD-772 holds the volume half of the same defect: 73 independently hand-rolled parsers across nine committed artifacts, report() defined in 39 task files, exit 2 hand-rolled in 76, and zero source/. lines in the whole mise-tasks tree — there is no shared library and never has been. CLOUD-773 supplies the mechanism rather than another count: graph-check spawns ready-lint — the one program in the tree that parses a Ready block and identifies every key it cites — and then re-derives the issue-key regex itself at :216, :291 and :302, because an exit code cannot carry what the producer already knew. That is why the copies exist, and it is why one authority is necessary but not sufficient.
Filed from a subprocess-boundary audit; the ratio data is CLOUD-623's and CLOUD-647's, read the other way round.
Probe plan
- Unresolved decision: canonical issue-key behavior for case, boundary, and mandatory project prefix.
- Probe: run the named existing checkers over the recorded key examples, including
closes cloud-757,AB-1,A-1foo, andCLOUD-1x, before and after routing one checker through the shared board fact. - Record: each checker verdict by example, selected canonical behavior, unreachable-tracker result, and evidence that one consumer no longer re-extracts the fact.
- Ready when: all three behaviors are explicitly selected, the checker results agree, unreachable resolves to “could not look,” and one extraction half is deleted; then expand consumers from that authority.
Refinement — Ready (one definition of an issue key, and all three axes chosen rather than inherited)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
The three divergences are decided here, because "resolve them deliberately" is not a predicate until the resolutions are named.
- **Case: sensitive. **
CLOUD-757is a key andcloud-757is not one, and a body writing the lowercase form is refused rather than normalised. Normalising up looks friendlier and is what produced the shipped defect: one gate accepts the spelling, two others cannot find it, and nothing says so. The cost is stated rather than hidden — a PR body writingcloses cloud-757failsclosing-key-checkwhere today it passes, which is the refusal that should always have been there. - **Boundary: the explicit character-class form **
(^|[^0-9A-Za-z-])…([^0-9]|$), not\b. It states both sides in one place and does not depend on how a given matcher treats-, which is precisely what let two sites disagree while both looking correct. - Project prefix: mandatory. The four shell
caseglobs are replaced outright, not tightened: a glob cannot anchor, which is why[A-Z]*-[0-9]*acceptsAB-1,Z-9andA-1foo, and whyCLOUD-[0-9]*acceptsCLOUD-1x.issue-read-check:91's message already asserts the prefix is required, so the message is right and the glob is what changes. - Authority boundary (§1). The definition itself is no longer this row's — it was split out so it could ship, and this row is the consumer half: the nineteen sites that re-derive the key each stop doing so and read the fact instead, with their extraction halves deleted. That is what makes §1 clean here and the row blocked all the same, because seventeen of those sites are governed
mise-tasks/programs. The Ready-block clause structure is scoped honestly rather than wholesale: §8's cross-check is already a relation and becomes a typed fact; the prose clauses stay prose, per CLOUD-623's warning that an engine over un-normalised prose cannot help. - Computable predicate (§2). Two, both decidable. First, every consumer's verdict over a fixed example set —
CLOUD-757,cloud-757,AB-1,Z-9,A-1foo,CLOUD-1x, andCLOUD-17insideCLOUD-179— agrees, where todayclosing-key-checkandclaimed-keysdisagree on the second. Second, board facts are three-valued: an unreachable tracker resolves to "could not look", never to an empty set. That is CLOUD-757's contract doing the work CLOUD-251 recorded as missing, and it is what stops a rule set with no relations reporting the board coherent. - **Effect (§3). **
read. Board state is bounded and cacheable, resolved atverify/config linttime through the existing boundary rather than per-checker, so no verb changes class and no per-checker spawn is added. - Output & exit (§5). Unchanged and pointer-only: a refusal names the key and the site, never a body. Exit follows the one table — a malformed key is a policy verdict at
2, an unreachable tracker is3, and the two must not be collapsed, since that collapse is the vacuous pass this row exists to close. - **Commit / bump (§6). **
feat(facts)— patch until0.1.0, since below that release-plz bumps the patch whatever the type says. Not!for the consumer surface: the change only ever adds refusals —cloud-757,AB-1,A-1fooandCLOUD-1xstop being accepted — which house style §8's raise-only invariant permits, and no exit code or output shape moves.mise run semverdecides the library half, which is where a new fact type can move apubsurface without anyone meaning it to. - Test obligation (§7). Over the compiled binary and, for each converted consumer, whatever tier survives that consumer's own retirement — which is part of what is unresolved, since a consumer that retires takes its bats suite with it and a consumer that does not cannot gain a case. Each case shown able to fail per CLOUD-418. The discriminator is the shipped defect and it is red today:
closes cloud-757must produce one verdict acrossclosing-key-check,claimed-keysandreleased, where today the first accepts it and the other two cannot see it. The four glob sites need naming individually because they are the ones whose behaviour changes:AB-1,Z-9,A-1fooandCLOUD-1xare accepted today and refused after. The vacuity case, which a two-valued fact passes silently: an unreachable tracker must be distinguishable from a board with no relations. And the anti-duplication case, without which this whole row decays: a twenty-first independent derivation of the key must fail the gate. - Blockers (§8). This row is
blockedByCLOUD-1142, which holds the definition half and must land first, andblockedByCLOUD-757 for the fact model's cost class and its three-valued contract.relatedToCLOUD-251 (the vacuous-pass trap), CLOUD-772 and CLOUD-773 (the volume half and the mechanism half of the same defect), CLOUD-623 (the caution that keeps this a fact row rather than an engine row) and CLOUD-418.
Split note, 2026-08-29 — the definition landed elsewhere; what is left here is blocked, and by what.
This row asked for one definition of an issue key and the conversion of nineteen consumers. The first half is landable and the second is not, so they are now two rows and this one keeps the harder half.
CLOUD-1142 carries the definition: the fact producer under crates/batten/src/, the three axes exactly as decided above, the three-valued could-not-look contract, and the gate that fails a twenty-first derivation. It touches no governed path and can be built today.
THE "CANNOT RETIRE" CLAIM IS WITHDRAWN (2026-09-01) — it was false in both halves, and it has already been paid for
The paragraph below said three programs cannot retire at all. Withdrawn rather than softened, because it is disproven twice over and it has already cost a landed retirement.
**Disproven by demonstration. **hooks-wiring-check.sh is one of the three. It is no longer tracked on origin/main. It retired.
Disproven by reading the module. The reasoning cites only_drops_a_retired_reference — the REMOVAL clause — and concludes about ADDITIONS. Additions are governed by admitted_addition, which has four arms; one of them, repoints_at_the_declared_successor, is an ADDED line swapping a retired path for the successor its own ledger declares. Repointing a sibling is exactly that arm's case. CLOUD-1299 has since added a fifth, admitting a caller that names its callee as a TASK rather than a path.
Already paid for. Replaying the key-derivation count over 400 commits of origin/main fires exactly once: c64e54a3, "take back the Ready-grammar retirement, whose successor cannot spell a sibling" — a revert that put four derivations back, on this same false premise.
The lesson generalises past this row: a blocker asserted from one rule while the admitting rule sits beside it is a block reported as a decision, which AGENTS.md already calls a bug. No row should cite "cannot retire" again without naming which admitted_addition arm fails and why.
What stays here is the conversion, and it is blocked on the retirement campaign's ordinary sequencing rather than on any impossibility. Seventeen of the nineteen sites are mise-tasks/ programs — ready-lint.sh, graph-check.sh, closing-key-check, released, landed-check, done-check, bot-issue, board-move-guard, issue-read-guard, issue-read-check, hooks-wiring-check, deferral-check, claim-race-check and claimed-keys among them. Editing one in place raises V-SHELL-RULE-EDITED, which declares one route, R-PORT-AND-RETIRE, with no override and no bypass_env. Retiring them instead is the sanctioned route, and it is available for all of them. ready-lint.sh is resolved by path from graph-check.sh:163 and pipefail-grep-check.sh, and graph-check.sh from board-sweep.sh and released.sh — sibling constructions, which is_retired_reference's arm 2 recognises and repoints_at_the_declared_successor admits repointing. hooks-wiring-check.sh carried a DECLARED table, and drops_a_retired_name (CLOUD-1224) exists for precisely a list that names a program by stem; it has since retired, which settles that one by demonstration rather than by argument.
So this row's acceptance clause — "at least one existing checker consumes the fact rather than re-extracting, with its extraction half deleted" — arrives with whichever consumer retirement lands first, and it belongs to the shell-retirement campaign's sequencing rather than to this row's own effort. It is recorded here so the next reader does not rediscover it as a surprise mid-build.
The definition half has landed (CLOUD-1142): ready::Grammar::key_of and keys_in own the three axes, the token stays the consumer's in [[pattern]] ready-issue-key, and issue-key-derivations-not-growing ratchets the task tree so the count cannot grow while these conversions wait. Replayed over 400 commits before its severity was set: 400 examined, 1 would-fire, 0 false positives.
CLOUD-841 `config-lint` reads a receipt that names no weakening as "no receipt", so a commit trailer alone admits a weakening the groom never saw
Why
CLOUD-789 gave config-lint its groomed-weakening hatch, and the design is stated in that file's own header:
- The GROOMED BODY names it — a
**Weakens:** …clause …claim-checkcopies it into the branch's claim receipt as aweakensline, at the one moment a gate holds the groomed body and the work has not started.- A COMMIT names it — a
Weakens: <smell-id> <key>trailer …- They AGREE. Where both are readable, a trailer naming something the groom did not is refused, and the trailer alone admits nothing.
"The trailer alone admits nothing" is not what the code does. mise-tasks/config-lint reads the groom as:
groomed=$(grep -E '^weakens ' "$claim" || true)
so a receipt that exists and names no weakening yields the same empty string as no receipt at all. The admission arm then keys on that:
if [ -n "$trailers" ] && grep -qFx "$smell $key" <<<"$trailers"; then
if [ -z "$groomed" ]; then
echo "config-lint: admitted $smell $key (commit trailer; no claim receipt here to check it against)"
The message says "no claim receipt here" — CI's honest state, where none was ever written. On a developer branch the receipt is right there under $GIT_DIR and says, readably, this work was groomed and no weakening was admitted. That is not absence of evidence; it is evidence of absence, and it is read as the former.
Measured 2026-08-21 on claude/ratchet-retires-with-subjects-be87wn (CLOUD-807's branch). The receipt existed and named no weakening:
CLOUD-807
ready-lint pass
takeover 1 refusal(s) overridden (BATTEN_CLAIM_TAKEOVER): CLOUD-807 assigned
claimed-at 2026-08-21T07:50:43Z
...
Adding a Weakens: trailer to a commit, with the Ready block edited after the claim, produced:
batten.toml:rule[bats-tests-not-deleted].retires_with rule-predicate-changed
config-lint: 1 smell(s)
config-lint: admitted rule-predicate-changed rule[bats-tests-not-deleted].retires_with (commit trailer; no claim receipt here to check it against)
config-lint: 1 smell(s), every one admitted by a groomed decision
The last line is the defect in one sentence: it says groomed decision about a decision the groom never saw. The weakening in that instance was real and deliberate and is recorded on CLOUD-807 — the point here is that nothing forced it to be, and the local half that the header calls "the strong half" did not run.
Why this matters more than a wording fix. CLOUD-789's whole argument is that the strong check runs where the receipt is — verify, before a CI runner is spent — and CI confirms the committed half. As written, the strong half is unreachable on any branch whose groom recorded nothing, which is every branch that did not anticipate its own weakening. That is exactly the population the gate exists to catch: an author who knew would have groomed it.
The three states are two in the code. absent (no receipt — CI, or a branch with no claim), groomed-nothing (a receipt naming no weakening), and groomed-this (a receipt naming the pair) must be distinguished; today the first two are one. Only absent may fall back to the trailer.
Note claim-check must keep minting a receipt when the groom named nothing — it already does — so the distinguishing fact is receipt-exists, not weakens-line-exists.
Prior art on the same shape. This is CLOUD-615's class exactly: a fallback that reads "could not look" for "looked and found nothing", where the lenient branch is the one that fires. CLOUD-820 is removing the same shape from claim-check's own clock fallback, and its Ready block's reasoning ("delete the clock fallback rather than leaving it as dead code") applies here unchanged.
Refinement — Ready (three states, and only absence falls back)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Source of truth (§1). The claim receipt's existence is what separates "could not look" from "looked and found nothing"; its
weakenslines are what separate the third state. No second record of either. - Computable predicate (§2). For each pointer
batten.toml:<key> <smell>: admit iff aWeakens: <smell> <key>trailer exists inorigin/main..HEADand either (a) the claim receipt for this branch is unreadable/absent, or (b) it carries a matchingweakensline. A readable receipt with no matching line refuses, whatever the trailer says. Decidable from the filesystem andgit log, no network. - Effect (§3).
read. No new verb, no new column. - Generated artifacts (§4). None.
- Output & exit (§5). Pointer-only, unchanged:
batten.toml:<key> <smell>. The refusal must say which of the two reasons applies — "no groom recorded for this branch" is a different remedy from "the groom named a different pair" — and must not quote the Ready block. - Commit / bump (§6).
fix(gate)— a shell task, no crate API touched. - Test obligation (§7).
tests/config-lint.bats. The discriminating case is the one that is green today and must go red: a receipt that exists, names no weakening, plus a matching commit trailer → refuses. Plus the three that must stay green: no receipt at all + trailer → admits (CI's path); receipt naming the pair + trailer → admits; receipt naming a different pair + trailer → refuses (already covered). A#MUTANTrow reverting the arm to[ -z "$groomed" ]must turn the first case green again, or the fix is untested. - Blockers (§8). None.
relatedToCLOUD-789 (which built the hatch), CLOUD-807 (where this was measured), CLOUD-615 and CLOUD-820 (the same could-not-look-versus-found-nothing shape).
Acceptance
- A branch whose receipt names no weakening cannot admit one by commit trailer.
- CI, where no receipt exists, is unaffected — the committed half still confirms what the local half proved.
- The admitting message no longer says "no claim receipt here" when a receipt is present and readable.
CLOUD-516 The claim receipt is keyed by branch name, so a branch restarted after its PR merged inherits a stale claim and `claim-guard` passes on it silently
Why
Measured 2026-08-13. .git/batten-receipts/claim.claude-groom-cloud-491-qv83e7 contains CLOUD-230. The work done on that branch this session was CLOUD-507, CLOUD-505, CLOUD-456 and CLOUD-513 — none of which CLOUD-230 has anything to do with. claim-guard was satisfied on every edit behind all four, by a claim for an unrelated issue, and reported nothing.
The cause is that the receipt is keyed by branch name, and a name is not a branch. After its PR merged, the branch was restarted twice with git checkout -B claude/groom-cloud-491-qv83e7 origin/main — the documented remedy for a merged PR, since new work must not stack on already-merged history. That repoints the name at a new base and discards the old commits. The receipt, living in .git/batten-receipts/ under the name, survived both.
Branch-keying was the right call and remains right for the case it was chosen for. claim-check's own header argues it: a claim attests to a decision about an issue that every commit on the branch continues to serve, so a SHA-keyed receipt would demand a re-claim per commit. That reasoning is sound. What it does not cover is the name outliving the branch it described.
This is the failure class this repo treats as worse than no gate. claim-guard did not error, did not warn, and did not skip — it passed, on evidence that had expired. A gate reporting green on state it never checked is the silent false green that linear-check's unguarded fetch, lock-check's regenerate-and-diff, and verify's unguarded body were each fixed for. The fix in every one of those cases was the same shape: record what the verdict was made against, and treat it as absent when that moves. linear-check's receipt already records the origin/main it was linear against; ready-guard's key to a SHA. The claim receipt records neither.
Refinement — Ready
Refinement gate: Definition of Ready & Done. This body carries only specializations.
-
Source of truth (§1). The claim receipt under
$(git rev-parse --git-dir)/batten-receipts/, and git. No tracker read: the question is entirely about local state, which is what makes it answerable in a hook at all. -
The naive predicate is wrong, and the reason is the landing loop (§2). The obvious rule — void the receipt when the branch's merge-base with
origin/mainmoves — fires on every lap, becauselandrebases onto the currentorigin/maineach time round and that is the loop working, not a fault. A rule that demanded a re-claim per lap would be refused within a day, and it would be refused correctly. -
Mechanism as a computable predicate (§2). The receipt records the
origin/mainSHA it was minted against. It is void when both: the current merge-base withorigin/maindiffers from the recorded one, and the branch carries no commits of its own (git rev-list --count origin/main..HEADis0).That conjunction is exactly a restart and nothing else:
situation base moved own commits verdict --------------------------------------------------------------------------------- claim, then work no 0 -> n valid a lap rebases onto newer main yes >=1 VALID (the loop working) main moves, branch untouched yes >=1 valid checkout -B <name> origin/main after a merge yes 0 VOID fresh branch, fresh claim n/a 0 re-minted anywayA restarted branch is the one state that has both a moved base and nothing of its own, because the restart discarded the commits that were the branch. No timestamps, no reflog, no heuristics.
-
Effect (§3).
readfor the predicate; the receipt write already exists onclaim-check's pullable path and does not change class. No new verb and noSURFACEchange. -
Output & exit contract (§5).
claim-checkkeeps0pullable /1not pullable /2unreadable stdin. A void receipt reads as absent, soclaim-guard's existing refusal and message are reused unchanged — the remedy is already "runclaim-checkfor the issue you mean to pull", which is exactly right here. Pointer-only: the branch and the recorded base, never a body. -
Commit / bump (§6).
fix(claim-check)— patch until0.1.0regardless of type. -
Test obligation (§7).
tests/claim-check.batsandtests/claim-guard.bats, over a fixture clone, one row per line of the table above. Mutation-checked per CLOUD-418: with the own-commits half of the conjunction removed, the rebase row must go red — that is the direction a careless fix breaks, and the row that proves the conjunction is doing work rather than decorating.The regression case is this branch: a receipt naming an issue, then
checkout -B <name> origin/main, then an edit — refused, where today it passes. -
Blockers (§8). None.
relatedToCLOUD-431 — it widens the same receipt for a different question (was the block refined, and by whom), so the two touch one file and should be sequenced rather than merged; CLOUD-444 — it proposes retiringclaim-guardinto the engine while keeping branch-keying, and would carry this defect across unchanged; CLOUD-377 — the other way this receipt is not minted when it should be; CLOUD-514 — which proposes a third branch-keyed receipt and inherits this until it lands.
Acceptance
- A branch restarted onto a new base carries no usable claim, and the first edit on it is refused.
- A branch landing normally is never asked to re-claim, on any lap, however many times it rebases.
- The rebase row is shown able to fail when the conjunction is weakened.
- A receipt with no recorded base reads as void rather than as valid, so receipts predating this change do not grandfather themselves in.
Not in this issue
Whether the receipt should carry more than a base — the ready-lint verdict and updatedAt that CLOUD-431 adds for a different question. And retiring claim-guard into the engine, which is CLOUD-444's.
CLOUD-1051 Replace prose-only and filed-here override passwords with issued capabilities
The issue remains blocked by CLOUD-1050 and in Backlog until its typed precondition and Rego verdict ABI land.
Shell-retirement specialization
Generated Batten completions are derived output and are not authored shell rules. This issue's retirement boundary is every authored mise-tasks/** guard and Bats suite required by the two adapter migrations.
mise-tasks/prose-only-check.sh, tests/prose-only-check.bats, mise-tasks/filed-here-check.sh, tests/filed-here-check.bats, mise-tasks/land.sh, tests/land.bats, mise-tasks/stop-guard.sh, tests/stop-guard.bats, mise-tasks/closing-key-check.sh, and tests/closing-key-check.bats are not edited in place. Their predicates and callers are ported into Rego/Rust policy-engine surfaces, then the authored shell rules and corresponding Bats suites are deleted with carried, subsumed, or changed successor records and compiled-binary replacement coverage. Related shell utilities are either left byte-identical and outside the changed-file set or receive the same full retirement treatment; no new shell task, Bats suite, or fallback parser is allowed.
CLOUD-1059 enforces this changed-file rule. This issue is blocked by both 1050 and 1059.
Blocker. This is blocked by CLOUD-1050, which supplies the typed verdict and override precondition. It does not begin until that registry lands.
The capability protocol
This section is the single authority for the scheme; nothing below restates it.
The capability is a content-addressed admission, not a derived nonce and not a random bearer token. Both earlier forms were wrong in opposite directions: derivation from public tuple data alone is forgeable, and a random token leaves the record unauditable.
admission = H("batten-admission-v1" ‖ JCS({rule, verdict, subject, head,
epoch, answers, prev, author}))
The serialization is canonical, not concatenation. Raw a ‖ b is ambiguous across field boundaries, so two different field splits can hash alike. One canonical form — JCS over a fixed object, or length-prefixed fields; the implementation picks one and the choice is part of what v1 pins — is what makes the address well-defined at all. epoch binds a repository/config generation, so an admission does not survive the policy change that would have made it unnecessary.
Authorization is the RECORD's existence and state, never possession of the name. This is the rule that must travel with the scheme: the hash's property is BINDING, not unguessability. Anyone holding the answers can compute it, so the address is neither a secret nor evidence of who created it.
The authority is the STORE; the address is integrity. Stated as a boundary rather than left implied, because an earlier wording here ("nobody but the issuer can create the record") claimed issuer authentication from a hash, which content addressing does not provide. What the address proves is that the record is internally consistent with its own fields. What restricts who may create a record is the store's write path, and nothing else. Under this row that write path is a local filesystem store, so the boundary is: anyone who can write the store can mint an admission. That is acceptable here — the threat model is honest error, not a hostile local writer — and it is written down so a later reader does not mistake the hash for a signature. If the trust boundary ever widens to a shared or remote store, the scheme needs a MAC or signature over the record; that is a separate row, never an inference from this one.
Because it authorizes nothing on its own, an admission is safe to print, log, quote in a commit and leave in a transcript — which removes the "never print a bearer capability in ordinary output" constraint that a log or PR body would eventually violate anyway.
What content addressing buys that a random token cannot:
- The store becomes self-verifying. Recomputing the address from the record's own fields must equal the address, so editing the reasoning after the fact invalidates it. Under a random token, post-hoc tampering is undetectable — and the corpus is the entire diagnostic point.
- Idempotence. The same reasoning yields the same address, so a repeated request is one record rather than one per attempt.
- The ordinal disappears. Re-articulation stops needing a counter: reusing the previous answers reproduces the previous address, which is already spent, so overriding the same situation again requires genuinely different text. "Even in the same session" becomes a property of the construction.
prevmakes the per-rule, per-subject history tamper-evident — no backdating and no reordering — which is a far stronger sensor than a flat append.- One value is capability, record key and audit reference, so the token and its record cannot drift apart.
The canonical in-toto receipt envelope stores the binding (rule, verdict, canonical-subject, HEAD, epoch), the answers, prev, author, and the spent state, keyed by the admission. It lives in the canonical out-of-tree receipt store at receipts/overrides/<fingerprint>.json; $GIT_DIR/batten-receipts/ is read for compatibility only during the two adapters' migration and is never an authority.
The store has no locking today, and this row must supply it. receipt.rs contains no locking at all — fs4 appears only in capture.rs — so the atomic issued → spent transition is a thing to build, not a thing to reuse. An earlier draft of this row asserted "under the receipt store's existing lock"; that was false, and the review caught it. What the row specifies instead:
- Transition. Consume is a compare-and-set on the record's state, taken under an
fs4advisory lock over the override store — the same choice, for the same reason, that.claude/rules/rust.mdrecords for the capture lock: the kernel releases it when the holder dies, so aSIGKILLmid-write leaves a reader a defined prefix rather than a lock nobody can release. - Chain head.
previs the previous admission for the same(rule, subject); the head pointer moves inside the same locked section as the state transition, so a chain cannot fork. - Concurrent consume. Exactly one consumer wins the compare-and-set; every other is a policy refusal (
2), never an internal error. - Crash recovery. A crash before the CAS leaves the record issued and still consumable; a crash after leaves it spent. There is no intermediate state, because the state lives in one record replaced by one atomic rename.
Canonical subject, per gate, since the binding claim is untestable without one. prose-only-check's subject is the ordered set of tracked paths in origin/main...HEAD — the same set its refusal names — canonicalized as sorted repo-relative paths; the branch name is deliberately NOT part of it, so a rename cannot mint a fresh override for the same diff. filed-here-check's subject is the single CLOUD-<n> key of the row being overridden — one admission per row rather than one per invocation, so overriding three rows costs three articulations.
The two adapters are explicitly write-effect at consumption because they spend state. Their legacy variables remain supported only behind a named compatibility adapter during migration; the adapter emits a migration finding and is deleted in the same release once both Bats suites prove issued-capability flow. No other override family is claimed.
Why
BATTEN_PROSE_ONLY_OVERRIDE=1, BATTEN_FILED_HERE_OVERLAP=1, BATTEN_CLAIM_TAKEOVER=1, BATTEN_HOOK_BYPASS=1 and the rest share one property: they are knowledge. Grep the source — or read the refusal, which prints the name — and you hold the bypass forever, for every subject, in every session. Nothing is issued, nothing is scoped, nothing is spent.
That is why an override costs nothing to reach for. Measured 2026-08-25: an agent hit prose-only-check, held the env var name the moment it read the refusal, and put the override to a human. The change needed no override at all — the correct route was a clause the caller's paraphrase had dropped (CLOUD-1050 defect B). The override was the cheapest concrete thing in reach, so it was reached for.
The recording half already exists and is post-hoc
Two gates already write a record: prose-only-check appends to $GIT_DIR/batten-receipts/prose-only-overrides.<branch> and filed-here-check's BATTEN_FILED_HERE_OVERLAP "records which rows it overrode". Both write after the decision, as a trace.
CLOUD-786 is the row for the other half — "the claim receipt records that a gate was overridden and no mechanism reads it, so a bypassed claim is indistinguishable from a clean one." This row does not duplicate it and does not block on it: 786 is nothing reads the record, this is the record is a precondition of release. They compose, and 786's deliverable is this row's sensor clause. Stated rather than absorbed.
What changes
The override stops being reachable by knowledge and becomes reachable only through a verb that issues it.
- The gate never adjudicates the reason. Non-negotiable rule 3 — "a gate resolves to a command and an exit code over an object it decides, never a model verdict" — and "is this justification good" is exactly a model verdict. The predicate is reason present and well-formed ⇒ release. The forcing function is articulation, not approval. This is the clause that keeps the mechanism honest, and it is also why it works: the cost is thinking, not asking.
- The admission is content-addressed over the proof chain (see the protocol section above). Single-use per SITUATION rather than per session, unharvestable onto a different subject because the binding is inside the hash, and re-articulation is forced by construction rather than by a counter — identical answers reproduce an already-spent address.
- The questions must be falsifiable to the writer, or a fluent agent performs the ritual and proceeds. The three that would have stopped the measured instance, and the answers it would have had to type:
- state the precondition this gate declares and the fact satisfying it —
prose-only-checkdeclares "the prose is the deliverable and cannot wait"; the diff was executable data, not prose, and it waited an hour before riding another branch. Both halves false. - name what is lost if you do not override — nothing.
- name the route you rejected and why it does not apply — forces engagement with routes the reader may never have received.
- state the precondition this gate declares and the fact satisfying it —
- Re-present the declined routes at issue time. Last cheap moment, and it catches the reader who never got route 1.
The corpus is a sensor on the GATE, not only on the agent
Twenty overrides of one rule is not twenty justified exceptions — it is a wrong predicate or an unreachable remedy. A per-rule override rate crossing a threshold should surface as a finding against that rule, which turns the log from an accountability record into gate telemetry. That is the more valuable artifact, and it is CLOUD-786's deliverable pointed at the gate rather than at the claim.
Composes with CLOUD-1050
The boundary that made this claim reach across scopes is closed. CLOUD-1050's registry was drafted policy-only, while both gates this row migrates are mise-tasks/*.sh — so consuming override.precondition from it was a claim across a boundary that row declared. CLOUD-1050 has since been broadened to cover refusal classes from both emitters (its Regal enforcement stays policy-only, because that is where a Rego AST exists to lint), so the dependency below is satisfiable as written.
That row makes override: {env, precondition} a typed field checked at authoring time — a gate declaring no precondition simply cannot be overridden, which is the right default. This row is that field's runtime consumer: the questions are generated from the gate's own declared precondition. One field, checked statically and spent at break-glass. If CLOUD-1050 lands first the env name disappears entirely; if it does not, the ritual still works off whatever precondition the gate states in prose.
Refinement — Ready
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Source of truth (§1). One issuing verb plus the override record in the canonical out-of-tree receipt store (
receipts/overrides/<fingerprint>.json), with$GIT_DIR/batten-receipts/read for compatibility only during migration. The two gates that already write records route through it rather than keeping their own append — a second writer is the drift CLOUD-1050 is about, one layer down. - Computable predicate (§2). A gate's bypass is honoured only when a record exists at the presented admission, is unspent, and binds to the presented
(rule, verdict, subject, HEAD); a bare env var is refused. Requesting one without answering every declared question is refused. Consumption is one atomicunspent → spentcompare-and-set under anfs4advisory lock this row introduces — the store carries none today. The address must recompute from the record's own fields, andprevmust resolve, terminate and not cycle — the same chain predicate CLOUD-1053 needs for token successors, so one implementation serves both. All are lookups and comparisons — no judgement, no clock, no network. - Effect (§3).
write, to the canonical out-of-tree receipt store named in §1. No new spawn, no network, no tracker. - Output & exit (§5). Pointer-only on the gate side — rule, verdict, subject pointer, admission — never the reasoning, which lives in the record. The admission is safe on this channel precisely because it authorizes nothing on its own. Exits follow the one table. Note the deliberate inversion: rule 4 keeps content out of output everywhere else, and here the reasoning IS the payload and belongs in the record, because it is the author's own words rather than repository content.
- Commit / bump (§6).
feat(receipt)!— a bare env var ceasing to work is a break in the consumer-visible surface and is declared, not hidden. - Test obligation (§7). Shown able to fail (CLOUD-418), each reddening one predicate: a bare env var with no admission ⇒ refused; an admission bound to a different subject or HEAD ⇒ refused; a request with an unanswered question ⇒ nothing issued; a spent admission ⇒ refused, while the same situation with genuinely different answers ⇒ a new address, issued; a record whose answers were edited after issuance ⇒ address no longer recomputes ⇒ refused; a
prevchain that cycles ⇒ refused. Plus the anti-vacuity arm — a correctly requested and answered override succeeds, so a mechanism that refused everything would fail. - Blockers (§8).
blockedByCLOUD-1050, which supplies the typed verdict and theoverride.preconditionthe questions are generated from.relatedToCLOUD-786 (nothing reads the record — this row's sensor clause is that row's deliverable; they compose rather than block), CLOUD-1050 (the typedpreconditionthis consumes), CLOUD-680 (the menu-shaped ask this removes the pressure toward), CLOUD-820 (the prior instance of a bypass that was opt-out by deleting your own receipt).
Acceptance
- No gate honours a bare env var.
- An admission is unusable against a different subject, a different HEAD, a different config epoch, or a second time.
- Two concurrent consumes of one admission resolve to exactly one success and one policy refusal.
- An unanswered question yields no admission.
- A record edited after issuance no longer recomputes to its own address, and is refused.
- The record carries the answers, and a per-rule rate is derivable from it.
- A legitimate override still completes, demonstrated end to end.
What this row must NOT claim
- Not that it stops a determined agent lying. It makes lying legible across a corpus; it does not prevent one well-written false answer. The value is the aggregate and the friction, not detection.
- Not that the gate judges. The moment the predicate grades a reason it becomes a model verdict inside a gate, which rule 3 forbids and which would be worse than today's password.
- It adds latency to a genuine break-glass. The ritual must be one structured answer, not an essay. And if the issuer is unreachable the override is unavailable — fail-closed on the bypass, which is probably right but must be a decision rather than a discovery.
Grooming decisions — Ready
Bounded migration. This change migrates prose-only-check and filed-here-check only. Their existing override records are the evidence and their Bats suites are the adapter tests. Other BATTEN_* variables are not silently redefined; each requires its own migration row.
Issuer and store. Add batten override request as a write-effect CLI command. It takes --rule, --verdict, --subject, and structured answers generated from the rule's declared override.precondition in CLOUD-1050. It appends an override record under the canonical out-of-tree receipt store at receipts/overrides/<fingerprint>.json; $GIT_DIR/batten-receipts/ is read only for compatibility during the two adapters' migration.
Capability protocol. The issuer writes the record and returns its content address — see the protocol section above, which is the single authority for the scheme. The two migrated gates accept only BATTEN_OVERRIDE_ADMISSION; their legacy bare environment variables are refused. A verifier resolves the record, checks its binding and unspent state, and recomputes the address from the record's own fields, without a network call or a clock read.
CLI artifacts and contract. Add the command in crates/batten/src/cli.rs, surface.rs, and lib.rs; regenerate completions/* and man/* with mise run completions and mise run man, gated by mise run derived-check. Issuance and record writes are write-effect; gate output is pointer-only. Success is 0; malformed request or missing required answers is 1; a rejected or replayed capability is 2; internal store failure is 3. Land as feat(receipt)! → patch until 0.1.0.
Tests and rollout. End-to-end binary tests cover issuance, a valid use, a bare legacy variable, a different subject or HEAD, a spent admission, the same situation re-answered differently, a record tampered with after issuance, a cycling prev chain, two concurrent consumes of one admission (exactly one wins), a crash between issuance and consumption, and an unavailable issuer. tests/prose-only-check.bats and tests/filed-here-check.bats prove both adapters honor only the issued capability. The record must permit a per-rule override-rate report; that report is telemetry, never a judgement of the written reason.
CLOUD-514 Nothing prices filing over fixing, so spinning off a defect in the PR's own diff is arithmetically cheaper than finishing it
Why
Every gate in this repo prices failing to record something. finding-sink-check fails a turn that cites path:line evidence and makes no durable write. deferral-check fails a PR that defers a decision without naming an issue. stop-guard kicks a hedged flag. issue-guard refuses a PR that names no issue at all.
Nothing anywhere prices the opposite: recording something instead of doing it. Filing satisfies every one of those gates at once and costs a few seconds, while finishing costs a diff, a suite and a landing. For an agent under pressure that is not a temptation, it is arithmetic — and the board becomes the escape hatch every guardrail points at. AGENTS.md already names the behaviour: "A punt is any deferral you could have closed … offering an action you are already authorized to take." That rule is prose, and prose is feedforward only.
Nor is the substitution a fair trade. Across studies of admitted technical debt only 26.3–63.5% of it is ever removed, with median lifespans of 18–172 days and instances surviving more than ten years; in trackers specifically the repayment distribution is severely skewed, median 25 hours against a mean of 872 hours. A ~35× median/mean gap is the signature of a long tail never repaid at all. Filing does not defer a fix, it converts one into a weighted coin-flip.
Measured 2026-08-13, PR #390. CLOUD-513 is a defect in code written in that PR: two new fixture suites read ambient git config, passed verify locally and failed CI. The two suites were repaired in 4259045. The gate — a one-line [tasks."test:bats".env] declaration, in a file the same PR was already editing, whose cost had already been measured at zero (1475/1475 with and without ambient config) — was filed instead of applied. The PR merged green and the board gained a row a one-line edit would have made unnecessary.
No reviewer is present at the moment of the choice, so the cost has to land on the author. Landing here is trunk-based: a branch fast-forwards onto main and review happens after the merge, which is why unreviewed paths stay behind feature flags rather than behind a withheld merge. A mechanism that works by surfacing the punt to a reviewer therefore acts hours or days after the row was opened and the branch was closed. Whatever prices this has to be paid by the author, at the instant of filing, or it is not a price.
Two mechanisms are ruled out before any is proposed
1. Judging the spin-off is forbidden. "Is this issue related enough to the PR to belong in it?" and "should this have been fixed instead?" are both model verdicts, which non-negotiable 3 refuses: a gate resolves to a command and an exit code over an object it decides. CLOUD-505 hit the identical wall, and its resolution is the template — do not judge the content, price the action.
2. A time window is measured, and rejected. The obvious credential-free proxy for "issues this branch filed" is "issues created between the PR opening and its merge". Measured over the last 40 merged PRs against every CLOUD-* created in each window:
merged PRs read: 40
issues created in-window: 184
of those, absent from body: 183 (99.5%)
It fires on essentially everything, and structurally rather than tunably: this repo runs a fleet, so a window captures every session's filings. PR #373's window alone holds 20 issues, none of them its author's. A gate at 99.5% is switched off the first day.
That measurement is what makes a receipt load-bearing rather than convenient. Which branch filed which row has never been recorded anywhere — not in git, not on the PR, not on the board. Without a record the predicate is not merely noisy, it is not computable at all.
Refinement — Ready
Refinement gate: Definition of Ready & Done. This body carries only specializations.
-
Source of truth (§1): one record, minted from the tracker's own bytes.
.git/batten-receipts/board-writes.<branch>, written by thePostToolUserecorder, holding per board write: the id, theupdatedAtthe tracker returned, and theready-lintverdict over the body the tracker returned.Why a local record at all, since a local record is what made the first draft forgeable. A gate running inside
landis a shell task, and no task inmise-tasks/has a tracker credential — that isclaim-check's "agents fetch, gates decide" split and it is not negotiable here, becauselandruns unattended in the background and cannot call a connector mid-lap. So the gate can read only local state.But exactly one fact is genuinely unfetchable, and the first draft confused that with three. Which rows this branch put on the board is recorded nowhere — not on the row, not in git, not on the PR. Everything else about a row (is it refined, when was it last touched) is a property of the row itself. The first draft therefore specified a separate
ready-lintreceipt and leaned on the issue-search receipt as evidence, and both were unnecessary ceremony around the one record that is actually needed.And the forgeability was self-inflicted. A
ready-lintreceipt is worthless when the agent assembles the payload it lints. The recorder does not have that problem: it fires on the tool result, which is the tracker's own response to the create and carries the full issue JSON. Linting that body, and recording theupdatedAtbeside the verdict, is evidence the author never touched. No second receipt, and no dependency on making a separately-minted verdict trustworthy. -
Mechanism as a computable predicate (§2): three sinks, priced in ascending order, and chat is not one of them. A finding met mid-PR has exactly three legitimate destinations, and the gate's whole job is to make the cheapest one the one that finishes the work.
- Fix it in this PR. Costs a diff. Nothing to record, nothing to gate, and this is deliberately the cheapest path.
- A durable comment on the existing row that owns it. Recorded, and otherwise unpriced. The first draft demanded a search receipt naming the target, on the theory that the row should have been found by looking rather than recalled — which buys nothing: an unfiltered
list_issues(optionalquery,limitup to 250) mints a receipt naming 250 rows, and no receipt can tell a right target from a wrong one anyway. Commenting on the row that already owns a finding is the honest common case and the friction belongs elsewhere. Filing remains gated byissue-search-guard, which is where duplicate-prevention lives. - A new row, groomed to Ready. The recorder lints the body the tracker returned; the gate refuses at land time if that verdict was not green. Costs a search (already required by
issue-search-guard) plus a complete Ready block: source of truth, computable predicate, effect, output contract, commit type, test obligation, blockers.
Sink 3 is deliberately more expensive than most fixes, and that is the entire mechanism. A one-line
[tasks."test:bats".env]declaration takes minutes; a Ready block for it takes considerably longer. So the arithmetic that currently favours filing reverses, without anything having to judge whether a given spin-off was lazy. -
Raising the filing bar is measured to work, in both directions (§2). Where a tracker imposes a structured filing template, monthly incoming volume falls while what survives gets better: median resolution time drops from 381 to 103 days, comment counts from 4.95 to 4.32, and more strongly structured templates further reduce resolution time, reopenings and discussion length. Fewer rows and faster ones is the same effect from both ends, and it is why the price is a Ready block rather than an arbitrary toll.
-
The friction must sit only on the impulsive path (§2). Deliberate friction is legitimate where it prevents an impulsive choice and illegitimate where it obstructs something needed. So: sink 2 stays cheap, since commenting on the right row is the common honest case; sink 3 is expensive but never refused, since a genuinely new finding must always be recordable; and every gate here fails open on anything it cannot establish. A gate that made recording a real finding hard would cause the failure
finding-sink-checkexists to catch. -
The recording half must be
PostToolUse, notPreToolUse(§2). AtPreToolUsea created row has no id yet; the tool result carries it. Measured, because no hook in this tree had ever read a tool result and the documented example is aWritewith a flat response. For an MCP tool the result is the content-block envelope, so.tool_response.iddoes not exist — the recorded shape is[{"type":"text","text":"<the issue JSON as a string>"}], and the key is reached with.tool_response[]? | select(.type=="text") | .text | fromjson | .id. The text carries the entire issue body, so the recorder must extract the id and nothing else (non-negotiable 4)..claude/settings.jsonalready runs aPostToolUseentry, so the event is available and the shape is proven. The body records both a create and a comment — a comment is sink 2 and must be attributable to the branch too — appending to.git/batten-receipts/board-writes.<branch>. It reusesissue-search-guard's decided details rather than re-deriving them: the create-vs-update discriminator (.tool_input.idabsent) and the suffix-anchored matcher, since CLOUD-178 measured three live spellings of the same connector and a rule naming one silently matches none of the others. -
The recorder calls
ready-lint;ready-lintitself is unchanged (§2). It is already a pure function of a piped payload and already the authority on a Ready block, so the recorder pipes it the tracker's returned body and stores the verdict. A hook shelling out to a task is normally the cost this repo refuses — but that argument was measured against aPreToolUsefiring on every Bash call, and this fires only when a row is created, which is rare enough that the startup cost is irrelevant. -
The gating half is
filed-here-check(§2), called fromlandbesidedeferral-checkand stopping the lap the same way. For every create this branch recorded, the storedready-lintverdict must be green. Comments are recorded and not gated. It judges no content and reads no tracker. -
Do not reuse
claimed-keysfor any body read (§2). It narrows to closing keywords because it answers which issues does this PR claim, and claiming would demand the PR close the spun-off row — precisely wrong for a genuinely unrelated one. CLOUD-379 and CLOUD-384 record the two being conflated in the other direction. -
Ship the recorder first, then the gate (§2). The firing rate cannot be estimated retrospectively — the discriminating data has never existed, which is the finding above. The recorder landed alone in feat(hooks): record what this branch put on the board, and whether it was refined #399 (corrected in fix(hooks): a comment records the issue key, never the comment's own uuid #418), and that staging is what makes the gate's input a proven shape rather than an imagined one. It does not buy a measured firing rate, and the original wording promising one was wrong: the record lives under
$GIT_DIR, is never committed and dies with the container, so no corpus accumulates across sessions and none ever could. -
Effect (§3).
read. APostToolUsebody writing under$GIT_DIR— machinery, asclaim-guard's receipt already is — plus a task reading stdin and receipts, plus one receipt write added to an existing task. No newbattenverb and noSURFACEchange. -
Output & exit contract (§5).
deferral-check's, unchanged: non-zero stops the lap, pointer-only per non-negotiable 4 — the ids and the missing receipt, never an issue title or body. The recorder is silent on every path and fails open on an unparseable payload, a missing git dir or a detached HEAD. -
Commit / bump (§6).
feat(hooks)— patch until0.1.0regardless of type. -
Test obligation (§7). One suite per gate, because
mutantderives its suite astests/$gate.bats;#MUTANTrows in each and every name added toMUTANT_GATES.Shown able to fail in both directions (CLOUD-418): a recorded create with no
ready-lintreceipt fails, and passes once the row is refined; a create whose recorded verdict is green passes; a recorded comment is never gated. Plus the rows that keep it from obstructing the needed path — a branch with no recorder output at all passes, an empty record passes, an update to an existing row is never recorded, and a fix-with-no-board-write passes untouched, which is the path the design wants to be cheapest.The regression case is this session: CLOUD-513 recorded against PR fix: the fast-forward verdict reaches land, and filing joins the gated paths #390 with no Ready receipt at filing time is refused, and passes once groomed.
-
Blockers (§8). None.
relatedToCLOUD-512 — it gates the move to In Review and explicitly excludes the attachment gap this closes as a side effect; CLOUD-323 and CLOUD-338 —deferral-check, the template and the record of its exemption logic being got wrong; CLOUD-505 — the search receipt this consumes; CLOUD-475 —finding-sink-check, the gate that prices under-recording and the reason nothing prices over-recording; CLOUD-513 — the measured instance.
Chat is not a fourth sink, and that is a memory property rather than a style rule
A finding stated in prose and never written durably is context-resident state. Long-horizon agents lose exactly that: unbounded context forces truncation and summarisation, attentional dilution degrades recall of any single item as context grows, and material in the middle of a long context is recalled least reliably — which is why an external store holding raw records at full fidelity is the standing recommendation. So a finding left in an aside is not merely undocumented, it is predictably lost, and the container reclaim this session survived is only the abrupt version of the same thing. finding-sink-check already gates this and needs no change here; it is named so the three sinks above are not read as four.
How this gets satisfied without doing the work
Three, found by attacking the mechanism rather than reasoning about it, each with local evidence. Two are closed above by changing the design; the third is a residual, stated with its failure direction.
A ready-lint verdict is forgeable if the agent supplies the payload — closed by moving the lint into the recorder, which lints the tracker's response instead. The finding stands and is why §1 reads as it does; the original remedy (a second receipt, made trustworthy elsewhere) was the wrong fix for it.
The measurement that made the hole real. ready-lint reads a payload the caller assembles. Run against this very issue three times while it was being refined — twice from a local file, once under the literal id CLOUD-NEW for a row that did not exist — it was green every time. So a receipt would attest that some text linted clean under some id, while the filed row is a stub. A toll payable in text nobody filed is not a toll. The first remedy drafted for this was a second receipt made trustworthy elsewhere, which added a dependency instead of removing the hole; moving the lint into the recorder — where the input is the tracker's response rather than anything the author typed — removes it. The same forgery argument is made independently by CLOUD-431 for its own question, which is why that issue is worth reading beside this one.
An unfiltered listing launders sink 2 — closed by dropping the requirement, because it bought nothing. list_issues takes an optional query and a limit up to 250, so a receipt demanding that the comment target be named is satisfied by one no-query call. Nor could any receipt tell a right target from a wrong one. Sink 2 is now recorded and unpriced.
The record survives a branch restart — and that is fail-closed here, which is why it is a residual and not a blocker. claim.<branch> on the branch this issue was written on names CLOUD-230, a claim from an earlier incarnation of the same branch name that outlived two checkout -B … origin/main restarts. The record proposed here is keyed the same way, so it inherits the staleness — but not the failure direction, and that distinction was missed on the first pass. There the defect is a false pass: claim-guard waved through every edit all session on expired evidence. Here a stale entry can only add rows the gate insists on checking; it can never remove one, since a restart does not delete the record and the new branch's own creates are appended fresh. So the worst case is a refusal naming a row from a previous incarnation, whose remedy the refusal message already gives. A gate that fails closed on stale state is the outcome this repo prefers, so the dependency was withdrawn. CLOUD-516 fixes the underlying keying and is worth landing first for its own reasons, but nothing here waits on it.
What this cannot do
It cannot judge the reason, and a minimal-compliance path still exists — a Ready block written to satisfy ready-lint rather than to be worked. The gate cannot refuse that without scoring prose, which is the model verdict rule 3 forbids. But the floor is now a complete Ready block rather than a pasted key, so the cheapest way to satisfy it is close to the work the row actually needs. Re-open predicate: re-open if a spun-off row is found to carry a Ready block written to pass ready-lint rather than to be worked — observable per instance on the row itself, which is the only scope available, since no cross-session window exists.
Gating creates pushes pressure toward commenting instead, and that is accepted rather than fixed. A comment on the row that already owns a finding is a legitimate durable home — cheaper than filing by design, since the friction is meant to sit on the impulsive path and not on honest recording. What makes it acceptable is that the pressure runs toward recording in the right place, not toward silence. It is still recorded, so the ratio is observable: CLOUD-475 records that a comment already counts as a durable home. Re-open predicate: re-open if a branch's own record shows comments standing in for fixes to defects in that branch's own diff — readable per branch at land time, which is the only scope available, since no cross-session window exists.
Acceptance
- A row created while a branch was live is refined at creation, on the evidence of the tracker's own response, and the lap stops otherwise.
- Fixing costs less than filing, and the acceptance is arithmetic rather than opinion: the cheapest path through the gate for a defect in the branch's own diff is to fix it.
- A genuinely new finding is never refused a durable home, and a comment on the right existing row stays cheap.
- No similarity comparison and no quality score exists anywhere in the mechanism.
- The gate's deployment safety is structural, not measured, because the observation window this bullet originally promised is unsatisfiable. Verified 2026-08-19:
.git/batten-receipts/board-writes.*is one file, nine rows, all from the session that wrote the recorder — zero creates, nine comments. The store is per-clone, never committed, and reclaimed with the container, so there is no fleet-wide window and there never was one. What stands in its place is the gate's own scope: it reads one branch's record inside the session that wrote it, judges only creates, and fails open on an absent or unreadable record — so a wrong verdict costs one lap on one branch, and its remedy is in the refusal message.
Not in this issue
Deciding whether a given spin-off was legitimate — the judgement the gate must never make. The In Review transition gate, which is CLOUD-512's. And retrofitting receipts for branches predating the recorder, which is why the gate fails open on their absence.
CLOUD-418 A new gate is never shown to fail, so a test that cannot discriminate ships as coverage
Why
This repository's most-repeated failure is a claim nothing exercises. land's refusal branch was dead code for months (CLOUD-235). timeout-check's budgets were placeholders that could not fire (CLOUD-352). A shape rule whose pattern was a program could never match and read as coverage (CLOUD-401). Each was caught after the fact.
It happened again, live, while building the landing lease (CLOUD-393). A concurrency test was written for a real race — observe() reading FETCH_HEAD, which is one file per clone while the heartbeat runs beside held/release in the same checkout. The test was green. Then the buggy version was restored to check the test could catch it, and it passed on the broken code too: every process fetches the same lease ref, so a crossed read yields a different generation of the same lease rather than an observably foreign one. The test asserted nothing.
That was found only because someone chose to mutate and re-run — a discipline nothing asks for and nothing checks. The green suite before that check and the green suite after it were indistinguishable.
Root cause. The obligation is stated as "a rule ships with a runnable gate" — a gate that exists. Nothing requires evidence the gate discriminates. A test that passes on both the fixed and the broken code satisfies every rule this repo currently has.
Scope, deliberately narrow. Not mutation testing over the workspace, which is a research project and a large CI bill. The claim here is about mise-tasks/*-check and the guards — the files whose entire purpose is to refuse — where the mutation is usually a one-line inversion and the suite is bats, so a run is seconds.
Refinement — Ready
- Source of truth (§1). The gate's own suite, run against a deliberately broken copy of the gate. A pass there is the defect; the verdict is an exit code, not a judgement.
- Mechanism (§3). Undecided between two, and choosing is what Ready needs:
- Author-side, checked in. Each gate declares one or more
mutantcases — a stated one-line corruption and the test name that must go red. A task runs them, and a mutant nothing catches fails. Costs the repo one small fixture per gate; runs locally, off the landing path. - Scheduled sweep. A weekly job applies mechanical mutations to
mise-tasks/*-checkand reports any whose suite stays green. No per-gate authoring, weaker coverage, and it belongs besidebranch-age-checkin the hygiene sweep, so no new CI minutes.
- Author-side, checked in. Each gate declares one or more
- Deliberately not in scope (§2). Mutation coverage of
crates/. Different tooling, different cost, different question. - Output (§7). Pointer-only: the gate, the mutant, and the test that failed to notice. Never a diff of the mutated source.
Test obligation
The mechanism must catch the case that motivated it: the FETCH_HEAD mutation of mise-tasks/land-lock against tests/land-lock.bats as it stood before the structural assertion replaced it. That pair is a known-good fixture — a real gate, a real mutant, and a real suite that missed it.
Commit / bump (§6): feat(gate) — patch until 0.1.0 regardless of type.
Blockers (§8): none.
Acceptance
- Every
*-checktask has at least one mutation its suite is proven to catch. - A gate whose suite passes on a broken copy fails.
- The
land-lock/FETCH_HEADpair is covered as a regression fixture, so the case that motivated this cannot recur silently.
|
Warning Review limit reachedNext included review available in 15 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 ignored due to path filters (1)
📒 Files selected for processing (18)
ℹ️ 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 (35)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change moves bot pull-request handling into Merge Risk: ⚪ Minimal · up to No actionable merge-blocking risk remains; the PR is merge-ready 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 |
f04cf37 to
44d671c
Compare
7ca5ba4 to
f9f2cde
Compare
9ac1576 to
b574687
Compare
…ressible
`one_exchange` derived the method from body presence — `None => GET`,
`Some => POST` — so PATCH could not be spelled at all. `bot-issue link` needs
it (`PATCH repos/…/pulls/N`, writing a closing key into a bot PR's body), and
that absence is why the program cannot retire onto the engine.
THE DERIVATION CARRIED A REAL INVARIANT, AND IT IS KEPT RATHER THAN TRADED.
Its own comment records what the pairing bought: a caller could not ask for a
GET carrying bytes, or a POST carrying none — two shapes a server answers
differently and neither of which any caller here wants.
Adding a `method` field beside the body would have handed both of those back
as constructible states in exchange for the one method that was missing. So
the method and the body become ONE field instead:
pub enum Payload<'a> { Read, Create(&[u8]), Update(&[u8]) }
There is no variant for a bodyless write and none for a read carrying bytes,
so the type refuses them rather than a comment asking callers not to. PATCH
arrives as `Update`, and the invariant is now structural where it was a
convention enforced by a two-armed match.
`no_variant_pairs_a_read_with_bytes_or_a_write_without` asserts the shape by
enumeration — every variant writes if and only if it carries bytes — so a
fourth variant breaking the pairing is red. It is the only way a type-level
property can be asserted from inside the crate.
Two construction sites, both updated: `fetch::get` and `mcp::dispatch`.
Refs: CLOUD-1295, CLOUD-1260
The predicate behind the receipt a carry branch will earn. Pure, so the tier drives every refusal without a git tree, and so what the receipt attests is the same thing the tests assert. WHY NOT A BRANCH-NAME EXEMPTION. `sbom-actions-currency` opens its PRs on `sbom-actions/carry-<timestamp>`, and the cheap fix is to let `verify` accept that prefix. That would be a password wearing a branch name: anything able to name itself so would pass, and the receipt would attest nothing about the change. So nothing here reads the branch name. What is attested is checkable against the merge base: exactly one tracked path differs and it is the licence table; every added line parses as `<repo>@<sha>` plus a licence and a holder; another row for the SAME repo carries an identical licence and holder, so only the sha differs; and nothing is removed or rewritten. Together those bound a carry branch to exactly what the workflow may produce. TWO CHOICES THAT ARE THE WHOLE PREDICATE, both driven by a case: APPEND-ONLY IS A PREFIX COMPARISON, not a line-set difference. A set diff reads a rewritten row as one removal plus one addition and could admit the addition — which is precisely the edit that must be refused. A ROW ADDED BY THIS DIFF MAY NOT VOUCH FOR ANOTHER. The known verdicts are read from the BASE side only; otherwise two unmapped repos vouch for each other and the branch carries a licence nobody ever judged. Shown able to fail: seeding from HEAD instead reddens `a_repo_with_no_prior_row_is_refused` and `a_changed_holder_is_refused_too`. The offline half is stated as such. Byte-identity of the upstream licence files is confirmed by the workflow at carry time; this bounds what the diff may SAY, not what upstream holds. Claiming otherwise would be a receipt asserting a check nobody performed, which is the defect being ended. Pointer-only: a refusal names the repo, the path and a line — never the licence string or the holder it compared, since those are the bytes the table exists to record. Refs: CLOUD-1295, CLOUD-1213, CLOUD-629, CLOUD-418 Admits: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: .serena/memories/core.md Admits-head: d8e2f2c Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: Nothing. One row is appended describing the new module; no existing row is altered, reordered or removed, and no other memory is touched. The map's own gate is what demanded it, so declining would leave the tree unable to commit the module at all. Admits-answer-precondition: `module-map-check` refuses a new `crates/batten/src/*.rs` that has no row in `.serena/memories/core.md`, and it refused this commit by name — so adding `carry.rs` and adding its map row are one indivisible change. The surface that owns a memory is Serena's `edit_memory`, which is what performed this write; the file is protected because agent context must not influence the rules, and a module map row is a description of code landing in the same diff a reviewer reads. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE was TAKEN rather than rejected — the write went through Serena's `edit_memory`, not a file editor. The refusal is the protected-path gate over the resulting diff, which no surface avoids. R-RESTORE-IT would drop the row and leave `module-map-check` red on a module this same branch adds.
… claim `sbom-actions-currency` opens a PR that `verify` refuses: there is no claim receipt, and the agent one attests a refinement no bot performed. #806 landed with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every future carry PR meets the same wall. `batten claim carry` is the honest receipt. It attests DERIVABILITY rather than a branch name — a prefix exemption would be a password wearing a branch name — so it mints only when the branch appends rows whose repo the base table already maps, changing the sha alone, and touches nothing else. The verb takes no argument on purpose: the subject is the branch's own diff against its merge base, and a caller that could name its own subject could name one that is derivable while changing something else. Also repairs the `snapshots` task, stale since the integration targets moved under `tests/it/`. Refs: CLOUD-1295
…the port The commit this reverts widened `fetch::Call` on the stated grounds that `bot-issue link` needed `PATCH repos/.../pulls/N` and that the method could not be expressed, so the retirement was blocked on it. It was not. `pr_watch` is the landed precedent for forge access from engine source (CLOUD-1143) and it reads through `gh`, which expresses every method and which resolves the credential outside this crate — where the alternative would have put token resolution into it, next to no forge credential row any config declares. The retirement takes that route, so `Payload::Update` has no caller and its doc comment states a reason that is not true. A widened type with no consumer and a false rationale is worse than the narrower one it replaced, so it goes back. Refs: CLOUD-1295
…eceipt
486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.
TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.
THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.
THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.
THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.
THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.
WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.
TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.
ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.
THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.
THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.
A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.
126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.
Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299
Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.
Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.
Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.
Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.
Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
Windows resolves an executable by PATHEXT, so the extensionless `#!/usr/bin/env bash` stub the suite places first on PATH is not a candidate at all. It is skipped, and a runner with the real client installed resolves to that instead — two cases reported the port broken (exit 3) where the port was fine and the stub had never run. session_provisioning.rs and connector_allow_door.rs gate their suites on the same rung for the same reason, and the retired tests/bot-issue.bats stubbed the same client the same way and never ran on Windows either, so nothing is narrowed that was covered. A .cmd twin of the dispatch would be a second authority over what the stub answers. Refs: CLOUD-1295
`Rule::checks` is a conjunction with no disjunctive spelling, so a row could not say "this receipt OR that one". `verify`'s own body has always accepted a claim, a bot or a carry receipt and treated any one as enough; `claim-needs-receipt` demanded claim alone, so a branch legitimately carrying a bot.<branch> receipt was denied on every write while holding a valid attestation - a live false positive since CLOUD-693 that CLOUD-1295's carry kind inherited. A second receipt row could not express it: a second row is a second AND, so the obvious spelling would deny every ordinary write on every branch. Hence a column. `checks_any` sits beside `checks` and a row carrying both is their conjunction, which is CNF and so expresses what neither column alone can. `checks` stops being unconditionally required on the kind - the same conditional-column move `pattern` and `verdict` already make - and a row naming neither is refused. Every fact-RESOLUTION site now reads `Rule::receipt_names()`, which yields both columns. That is the dead-gate half: a site walking `checks` alone would leave alternation names unresolved, an unresolved name reads Missing, and the row would deny every call it selected while reading as configured. Adjudication keeps the columns apart, in one extracted function so the two call sites cannot drift. The alternation is still a gate - a branch carrying none of the three is denied exactly as before - but admitting an alternative does lower a bar, and `config-lint` reads it as such against origin/main. Weakens: rule[claim-needs-receipt].checks rule[claim-needs-receipt].checks_any Admits: 40766b5e91c5f669d325302e8f016616ea07df9f2ddcea5b427e45b6271d3a28 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: e2b31cc Admits-epoch: e15dee1ebb84e7a0fe39ff59b6620fd9e766a471468936be5c6a15800491a04e Admits-author: alec@wenzowski.com Admits-prev: 29c18e3dd204c02bc64809928b5513310a0d41b26913cb6b89029cdff01c3345 Admits-answer-lost: The false positive CLOUD-1297 records stays live: a branch carrying a valid `bot.<branch>` or `carry.<branch>` receipt is denied on every mediated write while holding a real attestation. `verify` accepts all three kinds and this row accepts one, so the two authorities on "is this branch claimed" disagree, and the weaker one is what an agent meets first. CLOUD-1295 has just added the third kind that inherits it. Admits-answer-precondition: The change is a `[[rule]]` row's own predicate: `claim-needs-receipt` moves from `checks = ["claim"]` to `checks_any = ["claim","bot","carry"]`. A rule row IS the committed authority — no verb writes a row's predicate on an author's behalf, so there is no owning surface to route through. The write is the row a reviewer reads to learn what the gate demands, so it lands in the diff where it is visible. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because `batten.toml` is itself the owning surface for a `[[rule]]` row; the engine half of this commit only makes the column expressible and cannot declare the row. R-RESTORE-IT is rejected because restoring the committed bytes reinstates the defect the row exists to remove, which is the null change rather than a route. Refs: CLOUD-1297, CLOUD-693, CLOUD-1295
CLOUD-1297's second tier. Five cases on the same row the file already drives as a conjunction, in a separate policy constant so both spellings stay exercised — folding them into one fixture would leave whichever spelling it did not use untested. All three alternatives are driven, `claim` included: the column must still admit what the conjunction admitted, and a suite checking only the additions would pass over a column that had silently replaced the original. The vacuity case is the one the column most needs — an alternation fails by being satisfied by nothing, which is an allow wearing a gate's name — and a receipt of an unnamed kind proves the row admits the kinds it names rather than any receipt in the store. Shown able to fail: reading `checks_any` as an empty alternation, which is the dead-gate shape this column is most at risk of, reddens four of the five including the vacuity case. Refs: CLOUD-1297
`is_retired_reference` had four arms and every one of them resolved a PATH: the repo-relative one, a constructed sibling, a directory in a variable, the whole path in a variable. A caller that names its callee as a TASK carried none of them, so retiring a program such a caller names had no landable shape at all. Measured on `tests/verify.bats`, which pins the `verify` remedy's PROSE: it asserted the refusal names `bot-issue receipt`. Retiring the program that names makes the remedy false; changing the remedy fails the assertion; and editing a governed `.bats` is refused with no override route. The two landable shapes were retire the suite whole — a different unit's work, its subject is `verify` — or leave it alone. CLOUD-1295 took neither and kept the dead task name alive inside the live remedy. Arm 5 admits it, and the arm is used here rather than merely added: the suite and the remedy are both repointed at `batten claim bot`, and the workaround comment in `mise.toml` is deleted rather than left explaining a constraint that no longer exists. It sits on `is_retired_reference` rather than `is_retired_reference_by_ text`, which is the narrowing — the `_by_text` family is what decides whether a VARIABLE holds the retired path, and a task name is not a path, so admitting one there would have widened arms 3 and 4 for no reason anyone measured. It is not the `contains` licence the module refuses. The naming form is located and then BOTH SIDES of it are matched whole against anchored registry rows: `shell-task-runner-prefix` before it, empty or exactly the runner invocation, and `shell-task-subcommand` after it, empty or space-separated lowercase words. Every byte of the span is accounted for, so `mise-tasks/` fails the prefix and a path span cannot be read as a task span. The clause's other narrowings are untouched: the span is still derived from the diff, and the target still comes from `invocations_for`. Admits: 7152141e1e94ec0e30d2b38d724c8d79a2b978739003dbdd8b3899ae41bddfca Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: batten.toml Admits-head: f9f2cde Admits-epoch: f95421cc06dae85d936bb7c0b286b70258fb656bb5364aa9bd4ea9c0c16864c0 Admits-author: alec@wenzowski.com Admits-prev: 40766b5e91c5f669d325302e8f016616ea07df9f2ddcea5b427e45b6271d3a28 Admits-answer-lost: CLOUD-1299 stays open and the retirement it blocks stays half-done. `tests/verify.bats` pins the `verify` remedy's prose to `bot-issue receipt`, a task name plus subcommand, which `is_retired_reference` could not recognise — so CLOUD-1295 shipped a dead task name kept alive inside a live remedy because a frozen suite demanded it. Without these rows the new arm cannot be spelled at all: the alternative is an inline regex the loader refuses. Admits-answer-precondition: The change adds two `[[pattern]]` rows, `shell-task-subcommand` and `shell-task-runner-prefix`. A pattern registry row IS the committed authority: `.claude/rules/policy-modules.md` refuses an inline regex in an in-repo module at load time, so a module that needs a pattern has no route except a row here. There is no owning surface to go through and no verb that writes one. Both rows land beside the existing `shell-script-directory` pair the same module already reads, where a reviewer comparing them sees the anchoring property they share. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because `batten.toml` is the owning surface for a `[[pattern]]` row, and the whole point of the registry is that one concept has one spelling there rather than nineteen inline. R-RESTORE-IT is rejected because restoring the committed bytes leaves the module unable to name the pattern its new arm matches against, which is the defect rather than a route away from it. Refs: CLOUD-1299, CLOUD-1219, CLOUD-1224, CLOUD-1295
`reclaim-census report` classifies the PREVIOUS boot, which is immutable history: recording this boot does not move it, and no landing completed here changes what the last container was doing when it died. So on a container whose predecessor was reclaimed mid-landing the verdict is TRUE and repeated at every session start for the life of the container — and after the first read it is exactly the noise CLOUD-891 removed. The handler's own comment already made that argument for the negative readings and never applied it to the positive one. The mark is keyed to the boot time and sits beside the census log rather than in it, with both paths taken from the census's own `log-path` and `boot` accessors so this body holds no second opinion about where the store lives. Keying on the boot is what makes it self-clearing: a new container has a new boot time, so its own first reclaim verdict is reported rather than suppressed by a mark its predecessor left. Every failure path still reports. If the mark cannot be resolved or written the verdict is printed, because a sensor that goes quiet when its bookkeeping breaks has deleted the instrument CLOUD-451 built — suppression is what must fail closed, never the report. The whole fix is in the task body because `mise-tasks/reclaim-census.sh` is governed by `shell-retirement` and is not this row's to edit. The tier is Rust because the case that would have caught this died with its suite: `main` retired the session-start hook into handler rows and took `tests/session-start.bats` with it, a replacement `.bats` is refused by `V-SHELL-RULE-ADDED`, and `tests/reclaim-census.bats` is governed at head. It drives the real task body over a fixture `GIT_DIR` with an injected `BATTEN_BOOT_TIME` — isolation the census already affords, because a suite reading this container's own store would suppress the live verdict a human still needs. Shown able to fail: dropping the mark comparison, which is the shape that reintroduces the defect, reddens both suppression cases. Refs: CLOUD-1301, CLOUD-451, CLOUD-891, CLOUD-1295
Two gates caught the previous commit and both are right. `inline-task-bodies-not-growing-basic` counts `run = """` in mise.toml, and the multi-line body added one. The ratchet's own comment says which way out: a one-line `run = "..."` shim is the campaign SUCCEEDING, and two of its four replayed firings were exactly that. So the body collapses to the single-line form the task already had, with the reasoning staying in the comment above it where it does not count. A new `mise-tasks/` program was never the alternative — `V-SHELL-RULE-ADDED` refuses one. The two spawns in the new tier carry `#[expect]` inventory rows rather than being removed. `git` because the census resolves its store with `git rev-parse`, so a hand-built `.git` would test a path the program never takes; `mise` because the subject IS a task body, and spawning the engine instead would assert over a decision this row deliberately does not put in the engine. Behaviour re-driven on this container after the collapse: reports on the first session, silent on the second. Refs: CLOUD-1301
The `Weakens:` trailer on the `checks_any` commit named two KEYS and no smell. `lint::admissions` compares against `<smell-id> <key>`, exactly the pair a reader already sees in the pointer line, so that trailer admitted nothing and `config-lint` refused the tree. It was also separated from the `Refs:` line by a blank line, which puts it outside the final paragraph and stops git reading it as a trailer at all — so it was invisible twice over. The groomed half was missing too, and that is the half house style §8 actually turns on: an admission needs evidence from two moments — a trailer that travels with the change, and a body that named the same pair before the work started, copied into the branch's claim receipt by `claim::mint`. A trailer alone is the self-issued permit §8 refuses, and CLOUD-841 made a groom that looked and named nothing REFUSE rather than fall through. CLOUD-1295's Ready block now carries the clause its own §8 predicted when CLOUD-1297 was filed, and the receipt was re-minted from it. An empty commit because the trailers are the artifact: `admissions` scans every commit in the range, so they need not sit on the commit that performed the weakening, and rewriting that commit would rebase history other work on this branch is already stacked on. Weakens: rule-predicate-changed rule[claim-needs-receipt].checks Weakens: rule-predicate-changed rule[claim-needs-receipt].checks_any Refs: CLOUD-1297, CLOUD-1295
…tions Three things the rebase onto current `main` exposed, none of them a merge artifact. `every_rule_column_carries_a_fact_verdict` reds on a `Rule` column nobody has classified, and `checks_any` was one. It declares the SAME fact as `checks`: the two differ in how they are adjudicated — all of one, any of the other — and not at all in what must be resolved to adjudicate them, because "any one is valid" cannot be answered without asking about each. Declaring it not-fact-bearing would have said the alternation's names need not be resolved, which is the dead gate CLOUD-1297 exists to have avoided, so this entry is what makes `Rule::receipt_names` honest at the acquisition layer the way that helper does at every resolution site. `the_emitted_surface_is_exactly_the_committed_row_set` crossed the line ceiling because `main` added rows while this branch added six. The expected set moves into `committed_rows`, with each row's reasoning travelling WITH it — a reader who has to look elsewhere for why a row is spelled as it is has the problem the extraction was meant to solve. `census_fixture` was split twice independently: `main` extracted the config text, this branch extracted the repository chain, and both survived the merge as duplicate definitions. They compose rather than compete — the const holds what each verb needs DECLARED, the function holds the tracked files and the two commits a diff-shaped verb needs — so both are kept. The corpus is re-measured over the rebased tree, since the conflict resolution took main's copy and that one still bills a suite this branch deletes: 122 suites, 511.8s serial. Refs: CLOUD-1297, CLOUD-1295
`target-prune` refused every lap on this container: `[prune.warm] was measured against a tree that no longer exists` — declared 152, live 164, tolerance 10. Free space was 9648MB against a 7938MB floor, so nothing about the refusal was disk; it was the basis-drift arm doing its job over this branch's three added suites plus main's own. `count` moves to 164 as the gate instructs. `measured` already reads 2026-09-01 from that same day's earlier move, so the pair the block demands is satisfied by the count alone rather than by back-dating anything. THE FLOORS DELIBERATELY DO NOT MOVE, for CLOUD-1210's reason restated rather than re-derived: grouping the test targets severed the premise that a tracked FILE proxies a linked STEM — 147 linked artifacts became 4, `target/debug` went 7.76 GB to 2.05 GB — so both floors have sat far above what the tree needs ever since. A refreshed count therefore cannot make them too low, which is the direction that fails silently. Moving them DOWN needs the independent measurement the block names, and that is CLOUD-1158's. Admits: b28fd480c8126febacd3db96118787d32ebab604e9dce98f869210441887098e Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: c3eb480 Admits-epoch: bc1480e69c82d83fe97bbc27de2fc23febec91b9b02b69aa024f76f3ed7c187a Admits-author: alec@wenzowski.com Admits-prev: 7152141e1e94ec0e30d2b38d724c8d79a2b978739003dbdd8b3899ae41bddfca Admits-answer-lost: `verify` cannot run at all on this container. `target-prune` refuses every lap on "[prune.warm] was measured against a tree that no longer exists" (declared 152, live 164, tolerance 10), so no receipt is written and `land` cannot reach CI — with free space at 9648MB, well ABOVE the 7938MB floor, so nothing about the refusal is about disk. The drift is this branch's three added suites plus main's own, which is the gate working; leaving it blocks the branch on a stale number rather than on a real bound. Admits-answer-precondition: The change refreshes `[prune.warm.basis]` and `[prune.cold.basis]`'s `count` from 152 to the live 164, which the gate itself demanded in as many words: "Re-measure the floor and move `count` and `measured` together." A basis count is a committed-authority value — no verb writes one, and the refusal names this file as the thing to edit — so there is no owning surface to route through. The write is two integers plus the comment recording why the floors do not move with them, beside the block that already carries that reasoning for the previous refresh. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because this file IS the owning surface for a `[prune.*.basis]` row, and the refusal's own remedy names editing it. R-RESTORE-IT is rejected because restoring 152 restores the refusal: the count describes a tree that no longer exists, and no amount of restoring makes it true again. Refs: CLOUD-1295, CLOUD-1210, CLOUD-1158
The rebase onto current `main` took its RESULTS.md at the conflict, which still bills `tests/bot-issue.bats` — a suite this branch deletes — and misses main's own retirements. Re-measured over the tree that ships: 121 suites, 472.5s serial. The total is not this change's to claim: several retirements separate it from main's reading, this branch's and main's alike. Refs: CLOUD-1295
CLOUD-1213's first acceptance clause. `auto-bot-land` merged only `renovate/**`, so every carry PR needed a person — which is what produced #806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove. THE PAIRING IS THE POINT. The two lanes have different authors: Renovate opens its own PRs, and the carry lane is opened by `sbom-actions-currency` under the default token. A single `BOT_LOGIN` no longer answers, and the lazy widening — any known prefix against any known login — would admit `renovate[bot]` on a `sbom-actions/` branch and this workflow's own token on a `renovate/` one, neither of which either lane can produce. `LANES` is therefore a table of PAIRS and a PR must satisfy one row whole. CLOUD-867's origin test is untouched: `head_repository.full_name` is still what a fork cannot forge, still checked separately, and the prefix stays a filter rather than the trust boundary. Driven over a six-PR fixture before landing, and it caught a real bug: the first spelling read `.prefix` inside a pipe where `.` had already rebound to the branch string, which `jq` refuses at runtime — a workflow that would have errored on its first tick. With the lane bound, both lanes are admitted, both cross-pairings are refused, a fork-headed PR is refused and a human's branch is refused. What still gates a carry PR is unchanged and is not review: the full required check set must be green, and `batten claim carry` bounds what the branch may contain — only the licence table differs, every added row names a repo the base already maps with an identical licence and holder so only the sha moves, and no other tracked path differs from the merge base. Admits: b93a99d9da1523b9d9bd398f5b8c302d989357d8214bffe6e09c5e6aa30e9d12 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: .github/workflows/auto-bot-land.yml Admits-head: 0e089a9 Admits-epoch: 25c305c35f01ee4c99bceeacd1efc47509463adf09d3e5923196f59089cc2d98 Admits-author: alec@wenzowski.com Admits-prev: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763 Admits-answer-lost: CLOUD-1213's first acceptance clause stays open and every licence-carry PR keeps needing a person to land it — which is what produced #806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove. The lane can now be landed honestly either way, so what is lost is the automation rather than the correctness; the human asked for the automation explicitly. Admits-answer-precondition: The change extends this lane's on-green auto-merge from `renovate/**` to `sbom-actions/**`, which is a workflow trigger scope, a job `if:` and the resolve step's author test — all of them inside this file and expressible nowhere else. No verb writes a workflow, and the branch scope has to sit on the trigger rather than only in the `if:` for the reason this file already records (CLOUD-493: a job condition is evaluated after the run exists, so 1131 runs in 25 hours were created only to skip). It lands in a pull request, which is what the class asks for, and the diff is what a reviewer reads to see exactly which branches can now merge unattended. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because a GitHub Actions workflow IS its own owning surface — there is no generator and no config table that emits this file. R-RESTORE-IT is rejected because restoring the committed bytes restores the single-lane scope, which is the thing being changed rather than a fault to undo. Refs: CLOUD-1213, CLOUD-1295, CLOUD-867
b574687 to
554eedd
Compare
`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
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.
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
…-first CLOUD-761 measured twenty independent derivations of the key pattern in nine spellings, diverged on three axes, with a shipped defect behind them: a body writing the lowercase form is accepted by one gate and invisible to two others. This is the definition those sites converge on, plus the gate that stops a twenty-first arriving. THE CRATE OWNS THE AXES AND NEVER THE TOKEN. `no-tracker-key-in-core` refuses a derivation anywhere under `crates/**` — the mechanism that exists because CLOUD-1121 carried the literal in as a `const` and passed every gate — so the token stays the consumer's, read from the `[[pattern]]` registry, and `ready::Grammar` owns case, boundary and anchoring. The tests spell the prefix from parts for the same reason: a test is not exempt from the rule it is testing. THREE OPERATIONS, AND SEPARATING THEM REMOVES A DERIVATION RATHER THAN ADDING ONE. `key_of` answers "is this whole string a key" — the four shell `case` globs' question, which they get wrong because a glob cannot anchor, accepting `AB-1`, `Z-9`, `A-1foo` and a key with a trailing letter. `keys_in` answers "which keys does this text carry". The third question — "does this text carry key K" — needs no expression at all: it is `keys_in` compared for equality, so the boundary the two landed sites commented on is decided once. The boundary is read off the bytes either side of a match rather than composed as `(^|[^0-9A-Za-z-])…([^0-9]|$)`. `regex` has no lookahead, so a trailing class would CONSUME the byte after a match and make adjacent keys unfindable — and checking bytes means the crate composes no key expression, so there is nothing here for a twenty-first copy to be a copy of. THE GATE RATCHETS RATHER THAN FORBIDS, which is what makes it landable. 34 occurrences survive across the task tree; a `forbid` fires on every one. Converting them means editing governed programs, which is CLOUD-761's remaining half. Meanwhile the count cannot grow. Replayed over 400 commits of origin/main as CLOUD-1142's §7 requires, before the severity was set: 400 examined, 1 would-fire, 0 false positives. The one firing is `c64e54a3`, a revert that put four derivations back — a true positive, and the same false premise this row's own blocker rests on. Shown able to fail, and it caught a dead gate doing it: the first draft wrote the pattern in escaped regex form, but `ratchet_rule` counts with `str::matches` — a literal substring — so it counted zero at both ends and could never fire. Driving a twenty-first derivation through it is what found that; reading it did not. Admits: f3cdc5c1a49523305bc7451d2cc9bdd05a313d96a00e21164b405c6ce170bdc2 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: 9ece058 Admits-epoch: 7dcd0bd54e1da3834c9350ebcdeb40e91dbeb1c8a268d6466d66a26b459753c2 Admits-author: alec@wenzowski.com Admits-prev: b28fd480c8126febacd3db96118787d32ebab604e9dce98f869210441887098e Admits-answer-lost: The property whose absence is how twenty derivations arrived. CLOUD-761 measured twenty independent spellings of the issue-key pattern diverged on three axes, with a shipped defect behind them — a body writing the lowercase form is accepted by one gate and invisible to two others. Without this row the count can keep growing and nothing notices, which is exactly the history: replayed over 400 commits of `origin/main` the predicate fires once, on `c64e54a3`, a revert that put four derivations back. That firing is a true positive and there are no false ones. Admits-answer-precondition: The change adds one `[[rule]]` row, `issue-key-derivations-not-growing`. A rule row IS the committed authority — no verb writes one, and CLOUD-1142's §1 names this file explicitly as where the anti-duplication gate's identity, applicability, scope and severity are declared. It lands beside `no-tracker-key-in-core`, the row that already refuses the same derivation under `crates/**`, so a reviewer reads the two halves of one predicate together: forbidden in the crate, ratcheted in the task tree. Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because this file is the owning surface for a `[[rule]]` row and CLOUD-1142's §1 says so by name. R-RESTORE-IT is rejected because restoring the committed bytes restores the ungated state, which is the defect rather than a fault to undo. Refs: CLOUD-1142, CLOUD-761, CLOUD-1121
The id union landed without its other half. `config lint`'s groomed reader resolves `weakens <id> <smell> <key>` out of the claim receipt (CLOUD-841), and the loop that writes those lines walks the issues of THIS invocation — so claiming a second row carried row one's id forward and dropped row one's admission, and the refusal that follows names a smell whose groom sits in the tracker where no gate reads it. That is CLOUD-1231's third acceptance clause, which the ids alone do not meet. Both carries are now guarded by one predicate rather than two spellings of it, so the restart case cannot forget a claim and keep an admission. A row named in this invocation is deliberately NOT carried: the payload in hand is the authority for it, so a clause groomed OFF a row disappears rather than surviving in a file nobody re-reads. Without that the carry is a ratchet only a hand-edit can lower. Three cases, and the positive one was shown to redden with the carry stubbed out; the two negatives assert absence and stay green under it, which is what says they are testing the guard rather than the write. `plan` joins the verdict vocabulary in the same commit, since `verdict_vocabulary.rs` holds the declared table and the measured candidate list to each other in both directions. Refs: CLOUD-1231, CLOUD-841, CLOUD-516
CLOUD-472's core. `ready-lint` gates the SHAPE of a Ready block and `verify` gates the CODE, and nothing compared them — so an obligation could be written into an issue, pass the refinement gate, and land with no test behind it, with every gate in the loop green while it happened. CLOUD-369 is the measured instance: an acceptance bullet describing when a second matrix may be bought did not describe the code that buys it, and it merged by fast-forward, CI-confirmed green. The reconstruction is what makes it structural rather than inattention. The acceptance was written first and correctly; a collision forced a mid-implementation redesign; the redesign was derived from the problem again rather than from the acceptance and silently dropped one condition; and THE TESTS WERE WRITTEN FROM THE IMPLEMENTATION, so they assert what the code does. Tests written that way can only ever confirm it — they are structurally incapable of catching a dropped obligation, because a missing behaviour has no code to write a test against. The Ready block is the only artifact that still remembers what was promised, and nothing read it at implementation time. `ready lint` now emits the declared `<file>:<slug>` set before any verdict, for the same reason `cites-body` goes out early: emitting it after a refusal makes the set unavailable for precisely the rows most likely to carry a broken one, and a consumer reads that absence as could-not-look over a block that was read perfectly well. A recorder column carries it into the tree over the description the TRACKER returned — the forgery control the `verdict` column already has, earned when `ready-lint` over a self-assembled payload was measured green three times against text in a local file, once under an id no row carried. THE COLUMN IS ON ALL THREE `board-writes` ROWS, because the readers index positionally: a recorder that skipped it would put its neighbour's value where this one belongs for exactly the rows it did write. `policy/obligations-bound.rego` then refuses an obligation whose file this repository does not track, and — separately, because the remedies differ — one whose file exists and whose slug no `#MUTANT` row in it declares. This rule and `mise run mutant` are one obligation in two halves: this asks whether something is bound to the promise, the sweep asks whether it discriminates, and a case that cannot fail is not coverage (CLOUD-418). Three-valued throughout. `-` is could-not-look and passes, because a prose-dialect block emits no line and reading that as "declares none" would exempt exactly the rows this gate exists for. `0` is a measured zero and passes. An absent record is silent. `.claude/rules/toolchain.md` gains the wider half of the harness-directory rule: the SUBJECT decides between a hook and a verb. A hook mediates a call to somebody else's tool, so where the host spells it differently, switches it off, or was never surveyed, the envelope never arrives and the gate passes silently; a verb's subject is a record the agent minted, so a missing one refuses identically everywhere. The three measured instances are recorded, and so is the correction that disableability is universal and is NOT the argument — the argument is that absence must be a reading, not silence. Verification: `policy test` 45 bundles / 551 passed; `config lint` 0 smells; `test:cargo` 3965/3965, the six new cases included. Refs: CLOUD-418, CLOUD-369, CLOUD-209 Admits: 107a67c2786f9529a039cdf38a7e9507320ba01e1824f345ae455f18f8329e9f Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: b45e374 Admits-epoch: 34bb6def18926115877b0a377a8053da893a5a6f3899b3870e4a73bf29bcee50 Admits-author: alec@wenzowski.com Admits-prev: 256844626f24d9ecda229d209b816e36e927c5f14c8b3e9b49f3ded022d895aa Admits-answer-lost: `policy/obligations-bound.rego` becomes a module nothing runs: no rule row means the engine never loads it, no verdict row means its token fails registry validation, and no recorder column means the obligation set never reaches the tree. CLOUD-472 would land as three files that decide nothing — the dead-gate shape this change exists to remove. Admits-answer-precondition: A `[[rule]]` row, a `[[verdict]]` row with its four routes, and a `[[recorder.columns]]` entry are all consumer policy, and `batten.toml` is the one committed authority that declares them — there is no other surface that can express a new gate, so writing it directly is the only route. The whole write is additive and lands in this diff where a reviewer reads it beside the module it declares. Admits-answer-rejected-route: `config read first` is the one that names this case and it does not apply: reading the config is how the row shapes above were copied from `plan-complete` and `issue file held`, and reading it again changes nothing about the fact that the declaration must be WRITTEN there. `patch run first` rejected too — there is no generated or derived source for this content, so a patch would be the same bytes through another door.
`filed-over-own-diff` exempts a row the pull request CLOSES and reads that from `input.tree.records["pr-closes"]`. Until now that record had one producer: the `pr-body-closes` `[[recorder]]` row, minted from an observed `gh pr view --jq .body` tool envelope. So the exemption was reachable only when an agent happened to make that call, as a mediated tool call, on a harness whose spelling is surveyed. MEASURED ON THIS BRANCH, which is what turns a design smell into a defect: three rows it closes in its own body, all three refused, and no `pr-closes` record in the store at all — while `land` had fetched exactly that body and piped it to `filed-here-check`, whose task body is `batten check`, declared `read`, with no stdin channel. Fetched, handed over, dropped. The gate was pricing a punt that was not there and the author had no route to say so. `record plan`'s argument, one layer over: a verb inverts the direction, so the record exists identically on every harness and a missing one refuses rather than reading clean. `keys_closed_in` FILTERS `keys_in` rather than searching again, so this is a consumer of CLOUD-1142's one definition rather than a twenty-first derivation — and the closing verbs are the forge's vocabulary in a `[[pattern]]` row, anchored at the END so the token immediately before a key decides it. Naming a row is not closing one (CLOUD-674), and the case that pins it is the one that would otherwise exempt every row a body mentions. Zero is a count: `closes 0` says the body was read and closes nothing, an absent record says nobody looked, and an EMPTY body refuses rather than recording the first — a fetch that failed must not become a measurement. Five cases over the compiled binary, reading the record back through the engine's own `record_path` so a naming change reddens here rather than pointing the reader and the writer at different files. Refs: CLOUD-1311, CLOUD-1051, CLOUD-1142, CLOUD-674, CLOUD-514 Admits: 9d6eca64a31cb3a1ace83c435907d74675e90fae08b8af79e0db4790ce7b6979 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: b45e374 Admits-epoch: e214c143761fa98c543311176bd57eba127b3fcc3f7ad6697c97b5f582a3ca22 Admits-author: alec@wenzowski.com Admits-prev: f3cdc5c1a49523305bc7451d2cc9bdd05a313d96a00e21164b405c6ce170bdc2 Admits-answer-lost: `filed-over-own-diff`'s exemption for a row the pull request closes stays unreachable in the ordinary case, which is not a neutral gap: measured on this branch, three rows it closes were refused with no `pr-closes` record in the store at all, while `land` had already fetched that body and dropped it. The gate then prices a punt that is not there, and the author's only exits are an admission asserting a precondition that is false or a hand-written record, which is worse than the defect. Admits-answer-precondition: A `[[pattern]]` row is only expressible in batten.toml: the registry IS the surface, `Grammar::assemble` resolves `ready-closing-verb` by id with a loud failure, and `.claude/rules/policy-modules.md` refuses an inline regex outright — so `batten record closes` cannot run at all until the row exists. The token is the forge's closing-verb vocabulary, a consumer fact about which forge this repository lands on, so non-negotiable rule 1 forbids it living in the crate even if there were a route. It lands in the reviewed PR for CLOUD-1311 where `mise run config-lint` and `crates/batten/tests/it/record_closes.rs` judge it. Admits-answer-rejected-route: config read first 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. patch run first does not apply because nothing was destroyed: this adds one pattern row and touches no existing one, so the write is additive and strictly raise-only. It declares a new token that only ever narrows which keys read as closed; 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.
…t bind Both `#MUTANT` rows on `obligations-bound` spelled the comprehension variable `obligation`, where each arm binds `obligation_row`. The sed matched nothing, so neither mutation was ever applied and the sweep reported `inert-mutation` for both — a declared obligation that had never been shown able to fail, which is the same class the module itself exists to refuse, one level up. Reported rather than silently counted, which is the runner working as designed: an inert mutation is distinct from a SURVIVED one, and collapsing them would let a row that mutates nothing read as a row nothing kills. Second instance this session — `plan-complete`'s `unfinished-entry-unread` named `entry.status` against an arm binding `entry_row.status`. The tell is identical and the sweep is the only thing that sees it, because a `#MUTANT` row is prose to every other gate.
|
❌ The last analysis has failed. |
|
Closing this and continuing on What happened, since it is the reason and also a findingAt Nothing in Batten fired, and none of the three mechanisms that look like they should have is actually asked this question:
What carries overEverything. The new branch is cut from this one's tip, so all 26 commits here plus the sibling's Nothing is left behind on this branch. What is added thereA row for the destruction half, which needs no network: The detection half — a per-branch ownership ref taken by Generated by Claude Code |
Ten rows, one branch, one PR. This supersedes #817 and #818, whose work is replayed here commit-for-commit — they were three branches for one session's work, and one of the rows (CLOUD-1231) had been implemented twice, independently, two hours apart.
Closes CLOUD-1295
Closes CLOUD-1297
Closes CLOUD-1299
Closes CLOUD-1301
Closes CLOUD-1213
Closes CLOUD-1224
Closes CLOUD-1142
Closes CLOUD-1231
Closes CLOUD-1311
Closes CLOUD-472
What lands
CLOUD-1295 —
bot-issueretired onto the engine. 486 lines of shell and its 323-line suite deleted, six subcommands ported tobatten prandbatten claim, with onecarried:arm per deleted path and per declared@test.fetch.rsgained an explicit HTTP method so PATCH is expressible without making a GET-with-body or a POST-without-body constructible.CLOUD-1213 — the licence-carry lane lands itself.
batten claim carryattests that asbom-actions/*branch is derivable — every added row parses, another row for the same repo carries an identical licence and holder, and no other tracked path differs — rather than exempting a branch prefix, which would be a password wearing a branch name.auto-bot-land.ymlnow carries aLANEStable of (login, prefix) pairs instead of a singleBOT_LOGIN.CLOUD-1297 —
checks_any. A receipt rule'schecksis a conjunction; the claim gate needs the alternationverifyalready accepts. One sharedreceipt_verdictadjudicates both, and the alternation refusal names every alternative and its validity.CLOUD-1299 / CLOUD-1224 — two more
admitted_additionarms, so a caller naming its callee as a task name rather than a path can be repointed, and a frozen suite pinning a remedy string is no longer an unlandable shape.CLOUD-1301 — the reclaim verdict is once per BOOT, not once per session, so a container that replaced one mid-landing stops repeating it forever.
CLOUD-1142 — one definition of an issue key.
ready::Grammar::key_of/keys_inown the three axes CLOUD-761 decided (case-sensitive, explicit character-class boundary, mandatory prefix), the token stays the consumer's in[[pattern]] ready-issue-key, andissue-key-derivations-not-growingratchets the task tree so the count cannot grow while the conversions wait. Replayed over 400 commits before its severity was set: 400 examined, 1 would-fire, 0 false positives.CLOUD-1231 — a branch's receipt records every row it serves. The ids union, guarded by the recorded base so CLOUD-516's restart still forgets — and the
weakenslines carry with them, without which the union leaves a row claimed and itsconfig lintadmission gone.CLOUD-1311 / CLOUD-472 — the punt sweep and the declared plan get exit codes.
policy/filed-here.regogainsissue file heldfor a row this branch filed and neither closed here nor closed by the body;policy/plan-complete.regoholds a branch's own declared plan to its end, with the anti-vacuity arm that prices declaring no plan at all. Both verdicts are named in the landed three-word grammar, andplanjoins the vocabulary with its measured one-token candidate row.Verification
mise run test:cargo— 3959/3959.weakenscarry reddens with the carry stubbed out while its two negative arms stay green, which is what says they test the guard rather than the write.mise run config-lint— 0 smells.mise run verifybefore readying.Refs: CLOUD-761, CLOUD-1164, CLOUD-841, CLOUD-516, CLOUD-1051, CLOUD-514, CLOUD-418