Skip to content

CLOUD-843 bundle G: retire gh-guard, memories-check and rules-drift; four members refuted with measurements - #796

Merged
wenzowski merged 4 commits into
mainfrom
claude/cloud-843-bundle-g-yvi8p9
Sep 1, 2026
Merged

CLOUD-843 bundle G: retire gh-guard, memories-check and rules-drift; four members refuted with measurements#796
wenzowski merged 4 commits into
mainfrom
claude/cloud-843-bundle-g-yvi8p9

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Bundle G of CLOUD-843's shell-retirement campaign. Three programs retire; four candidate members are refuted, each with a measurement rather than a reading.

DO-NOT-CLOSE

The marker is bare rather than keyed, because this PR completes none of the eleven rows it names, and a keyed marker would exempt only those it lists. Which half of each it lands:

  • CLOUD-1163 has eight units; this lands two (7 and 8, the latter as a retirement whose successor was already in the tree).
  • CLOUD-1150 owns the rules-drift gate. This lands its retirement half — program and suite deleted, four predicates ported, $MUTANT_GATES entry dropped — and not its §2 deliverable, a new predicate over a rule-name set asserting a closed count. That predicate is the row's actual subject and is not attempted here.
  • CLOUD-1164, CLOUD-1194, CLOUD-1268, CLOUD-1061, CLOUD-825 receive recorded verdicts, not fixes.
  • CLOUD-1049 is reopened by this PR, not closed by it.
  • CLOUD-312, CLOUD-1027, CLOUD-843 are cited as context.

What retires

member programs suites @test cases successor
1 gh-guard, gh-guard-check tests/gh-guard.bats 23 crates/batten/src/hook.rs kind:mechanism — the successor was already landed
3 memories-check tests/memories-check.bats 11 policy/memories.rego
6 rules-drift tests/rules-drift.bats 30 policy/rules-drift.rego

Four programs, three suites, 64 case arms, three $MUTANT_GATES drops. bench/suites/RESULTS.md falls from 136 suites to 135.

Member 1 is the retirement whose successor landed first: batten.toml's four gh lifecycle rows have been the ported gh-guard since CLOUD-312, and both programs were unreachable from every hook, task and workflow for that whole span. The deletion removes a second implementation, not a gate.

What does not port, and why

Each of these was written, run, and backed out. The verdicts are on their rows.

tree-clean (member 4) — a fourth expressibility axis nothing in the campaign names. A [[rule]] whose subject is repository STATE has no glob, so it has no scope: tree-clean asks "does the working tree differ from HEAD", which names no path, and the row therefore speaks in every fixture repository inheriting this config — all of which are dirty by construction. It broke cli.rs::a_tracked_instruction_may_not_prescribe_the_denied_commit_identity by adding a second pointer. severity = "warn" does not save it, because the corpus asserts pointer counts. Beyond landable and expressible there is scopable, and this is the axis. Recorded on CLOUD-1164.

unlanded-check (member 5)crates/batten/src/lib.rs's stop_nudges spawns the program directly, so deleting it disarms a Stop nudge silently. Real work, not a wrapper repoint. Reverted; recorded on CLOUD-1164.

suite-bench-check (member 2)suite-bench is a producer with no predicate, so it has no policy surface to carry and V-SUCCESSOR-NO-SURFACE refuses every arm but a withdrawal. It cannot simply stay alive either: tests/suite-bench-check.bats declares both subjects and SubjectFacts::died is .all(). Recorded on CLOUD-1268.

lock-complete (member 7) — refuted by measurement. Format::for_path splits on the last dot and searches Format::extensions(); no variant owns lock. So a declared .lock is NotAcquired::UnknownFormat at rules.rs:7127 before a byte is parsed, even though staged_facts read the index bytes fine and they are valid TOML. input.tree.staged["mise.lock"] is undefined, Rego reads undefined as does not hold, and every predicate over it is silent — a module that loads clean, passes its own policy test tier, and enforces nothing.

a_declared_lock_path_never_reaches_the_staged_key in crates/batten/tests/staged_facts.rs pins it in both directions. No claim is made about policy/lock-entry-complete.rego, which reads the same key; I have no sound evidence either way about it.

The engine finding this PR surfaced

Writing member 6's could-not-look arm found that it cannot fire, and the reason is worse than an unfilled channel. An unreadable declared source stops the WHOLE RULE evaluating, silencing predicates that never read it.

Every prior probe on this class used a module whose only clause read input.tree.missing, which cannot distinguish "the channel is empty" from "the module never ran". An unconditional second clause separates them:

declared source a predicate reading NOTHING exit
valid fires 2
present, unparseable silent 0
absent, via documents silent 0
absent, via sources (glob) fires 2

-vv on the silent rows reports rule cost: probe 0ms 0 file(s). The same three-way over lines behaves identically, and rules.rs:7129 pushes the UnknownFormat cause into missing where it does not arrive — so the loss is downstream of the push, which is where a fix has to look. §1 of CLOUD-1049 currently points at the acquisition sites.

That last row is the usable part and is what member 6 ships with: the glob form keeps the rule alive across an absent file, so the module raises its own could-not-look from not input.tree.documents[path]. Nothing recovers the unparseable case.

CLOUD-1049 is reopened to Backlog with the reproduction. It was already reopened once on the same class and re-closed the same day; this is a third independent measurement, plus a fourth from staged. Its §2 acceptance needs one more clause — a rule whose declared source cannot be read must still evaluate its other predicates — without which a fix could fill missing correctly and leave the disabling behaviour in place.

Two corrections to prose that was present and wrong

Both are the exact class rules-drift exists to catch, and neither is one it can catch.

  • .claude/rules/policy-modules.md told every module author that "CLOUD-1049 shipped on 2026-08-25 and that parenthetical is stale" — i.e. that the could-not-look channel works. It does not. Replaced with the measured table, the sources workaround, and the instruction to assert whichever half you get in the compiled tier.
  • .claude/rules/toolchain.md claimed the four gh rows declare BATTEN_GH_GUARD_BYPASS as bypass_env. batten.toml:273 says they "WOULD DECLARE … AND DO NOT YET", deferred to CLOUD-1027. The test was written against the rules file, went red against the tree, and the tree was right.

Three decisions taken here, and what each cost

Stated rather than absorbed. All three are settled in this diff; none is left open.

  1. Member 6's four anti-vacuity guards changed shape, and one fidelity loss is accepted. The predecessor exited 1 when an authority would not read, catching both an absent and a malformed one. The successor catches absent and not malformed, because the engine defect above makes the malformed case unreachable from any module — CLOUD-1049 is the row that closes it, and an_unparseable_authority_is_silent_today_and_that_is_an_engine_gap asserts the measured behaviour with an instruction to invert it when that lands. Conditioning the guards on a claim existing is what keeps the row from speaking in every fixture, which is tree-clean's defect avoided rather than survived. Six changed: arms record the shape change.
  2. regorus carries no walk, so predicate 3's recursive descent became one fixed path (measured: each generated schema carries exactly one such node). That buys a vacuity hole a moved schema would fall into silently, so schema_vacuous covers the read-but-empty case in this diff.
  3. The ${VAR:-N} tie-break narrowed from head -n1 of grep order to "matches any observed default", because an object comprehension raises on a duplicate key. Strictly the more conservative direction — it can only report fewer drifts, never more — and no variable in the tree is read with two different defaults today.

Dispatch claims re-derived and corrected

The dispatch asked for every second and membership claim to be re-derived. Every per-suite second checks out exactly against bench/suites/RESULTS.md. Three claims do not:

  • Part 1 (CLOUD-1194, ten board gates) is not dispatchable. input.tree.captured reduces to one boolean, integer or bounded token per literal issue key hardcoded in committed config; the ten gates decide over a runtime-varying get_issue payload set and, in graph-check's case, the blockedBy relation graph. A relation graph over an unknowable id set is none of those three. The ledger is 346 case arms besides.
  • The could-not-look arm is exit 2, not 3, in every Part 1 gate. Exit 3 appears once, at board-sweep.sh:339, in a different lane; released.sh:117-128 exits 0 on an absent payload.
  • Part 3's pairing is wrong: perf-gate and perf-compare are two separate one-subject suites, so they do not die in one delta.

Confirmed as stated: board-sweep has no invoker, and graph-check's receipt-carries-no-ids mutation names a case that no longer exists.

Two commits carry an articulation block

CLOUD-1278 landed mid-session: a commit writing a protected path carries the reasoning in its message. Both batten.toml writers do, minted through batten override request rather than hand-written, and each is a distinct admission — the second chains prev to the first, because reusing the same answers reproduces the same address and reads as a replay rather than an articulation.

Verification

mise run verifyfast-forward-green. Underneath it: 3539 cargo tests, 2041 bats, policy test 38 bundles / 431 cases, mutant-census 110 gates, regal lint 34 files clean, opa check -s schema/ policy/ clean, suite-bench-check 135 suites, batten check clean on every rule.

Refs: CLOUD-843, CLOUD-1150, CLOUD-1163, CLOUD-1164, CLOUD-1194, CLOUD-1049, CLOUD-1268, CLOUD-1061, CLOUD-825, CLOUD-1278

@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
CLOUD-843 The retirement campaign has no row: eight capability rows cite "so the 79 gates have somewhere to migrate onto", and nothing owns the migration — measured, the bash grew today

Why

Eight rows in this campaign are justified by a migration nobody owns. CLOUD-833's title is literally "so none of the 79 gate-described mise-tasks has a surface to migrate onto"; CLOUD-832 exists so a bundle can carry 79 predicates; CLOUD-807 built the permit that lets a suite die with its subject. CLOUD-312 owns the 11 hook bodies and only those.

The 82 gate-described mise-tasks/ programs have no owning row. Searched 2026-08-21; the closest hits are all capability rows citing the campaign as their justification. This is DoR §2's own failure mode one level up: the backlog grew the engine, and the thing the engine was grown for was never filed.

Measured on main @ 12cca46 (v0.0.99), against the same census that morning

Surface Morning Now Δ
mise-tasks/ files 133 136 +3
mise-tasks/ lines 27,799 28,590 +791
gate-described tasks 79 82 +3
tests/*.bats lines 34,654 36,052 +1,398
bats cases 2,485 2,548 +63
gate tasks migrated 0
kind = "policy" rows 0 1 — and it is trunk-based-preset, a vendored preset, not a migrated gate

One bash file was retired all day: .claude/hooks/batten-hook.sh, 65 lines (CLOUD-824). Against +791 lines added. The campaign to delete bash added bash, and nothing on the board was positioned to notice, because no row carries the number.

The classification, by what each task invokes

Not by substring. A string scan over these files put ci-local-parity and pipefail-grep-check in the forge bucket — the same instrument that counted 14 spawn sites where name resolution found 9 (CLOUD-743). Classified instead by the external programs each task actually invokes in command position:

bucket count can it migrate?
tree — no git, no forge, no build 22 yes, on CLOUD-833's tree surface, today
git facts 50 needs a git fact on the tree document; scope unproven
build/bench 3 probably never — they run cargo/hyperfine
forge 7 last, and some may legitimately stay

The 50 is the number that matters and it was not expected: this is a git policy engine, so most gates read git. The "79 gates become Rego rows" framing every capability row inherited is unproven for 60 of the 82. That is this row's first deliverable to settle, not assume.

The pilot

mise-pin-agreement"every tool version named in .mcp.json agrees with mise.toml's pin — the second place a pin is written cannot drift from the first".

Chosen on data: 107 lines of bash, 10 cases, 108 lines of bats — the smallest of the structured-document gates. Both inputs are formats Fact::Document already parses (CLOUD-772: TOML/YAML/JSON/JSON5), and the predicate is agreement between two parsed trees, which is what Rego is for. No git, no spawn, no stdin.

It also carries no #MUTANT directive, so it is one of CLOUD-480's undeclared gates. Migrating it should add a declared mutation — coverage improves as a side effect rather than degrading.

Rejected as pilot: no-docs-tree, which an earlier plan named. It is not a mise-tasks/ program at all — it is a batten.toml rule in the hk gate. Recorded because the name was carried in prose across three documents before anyone checked the tree.

Sequencing, and the one trap

  1. Settle the 50. Determine what a git-fact gate needs on the tree surface. Until then "79 gates migrate" is an estimate, not a plan.
  2. CLOUD-835 lands — the destination for the 1,570 bats cases.
  3. The pilot, end to end, one gate: Rego module, test_ rules, delete the task and its suite, MUTANT declared. The pilot converts the estimate into a measured cost per gate. Do not batch before it.
  4. Waves by bucket, cheapest first.

The trap, and it is new as of today. CLOUD-807 landed retires_with, so a suite may now be deleted exactly when its declared subject dies. That is correct and it was the precondition for retiring anything — but it makes migrating without CLOUD-835 worse, not better. The ratchet will admit deleting a suite whose task died, with nothing asserting the Rego that replaced it. The permit made coverage evaporation quiet. 835 is a hard blocker, not a nicety.


Refinement — Ready

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Source of truth (§1). The tree. The census below is the authority on progress, not a burndown restated anywhere; batten.toml owns which gates are policy rows and mise-tasks/ owns which are still bash. No second list of what has migrated.
  • Computable predicate (§2). The census, re-run at every wave boundary:
ls mise-tasks/ | wc -l ; cat mise-tasks/* | wc -l
grep -l '#MISE description="Gate' mise-tasks/* | wc -l
ls tests/*.bats | wc -l ; cat tests/*.bats | wc -l ; grep -h '^@test' tests/*.bats | wc -l
grep -c 'kind = "policy"' batten.toml

A wave that does not move these down has retired nothing, whatever else it landed. That predicate is the whole point of this row existing: today's +791 was invisible because nothing computed it.

  • Effect (§3). read — migration moves predicates between surfaces; no verb is added and no rule kind's Authority changes.
  • Generated artifacts (§4). schema/batten.schema.json only if a row key changes; the per-wave batten.toml rows are authored. derived-check and schema-check gate both.
  • Output & exit (§5). Unchanged — a migrated gate keeps its exit contract, and a Rego predicate reports pointer-only by construction. A migrated gate's refusal text must still name its remedy (CLOUD-437); a msg that lost the remedy in translation is a regression the bats case would not catch.
  • Commit / bump (§6). refactor per wave — no bump. The answers are identical by construction; a wave that changes a verdict is not a migration.
  • Test obligation (§7). Per wave, and the pilot establishes the shape: (a) every case in the retired suite has a test_ rule that fails when the predicate is wrong; (b) the retired task and its suite are both gone, admitted by retires_with because the subject died; (c) the gate's declared mutation is caught by mise run mutant — for mise-pin-agreement that is a mutation it does not have today; (d) the census moves down by the retired count, asserted rather than eyeballed.
  • Blockers (§8). blockedBy CLOUD-835 — see the trap above; without a test destination the permit lets coverage disappear silently. relatedTo CLOUD-833 (the surface, landed), CLOUD-832 (predicate ids, landed), CLOUD-807 (the permit, landed), CLOUD-312 (the 11 hook bodies — the other half of the retirement, and not this row), CLOUD-480 (the undeclared gates a migration should shrink), CLOUD-772 (the document substrate the tree bucket consumes), CLOUD-839 (the capability dispatch that bought the machinery).

Acceptance

  • The 50 git-fact gates have a stated verdict: migratable on a named fact, or not, with the reason.
  • mise-pin-agreement is a policy row; its task and suite are deleted; mutant catches its declared mutation; the census is down by one gate and ~215 lines.
  • The measured cost per gate from the pilot is recorded here, and the waves are sized from it rather than from the count.
  • Every wave re-runs the census and records the delta.

Found while auditing what the CLOUD-839 fleet landed, by asking the question the capability rows never had to answer: how much bash actually went away.


PRESSURE-TESTED 2026-08-21 — wave 1 cannot dispatch yet, and the reason is a run rather than a reading

CLOUD-835 landed (62719ff, v0.0.100), so the blocker in the trap above is cleared and the 1,570 cases have a destination. Before dispatching a wave on that, the path was walked end to end against the release binary in a throwaway git fixture. It does not hold yet.

What the run showed

Two modules in one enabled bundle. One copied verbatim from policy.rs's own module doc; one written against what rules::tree_document actually builds. A stray.o tracked. Each with a test_ rule in the shape the vendored presets use.

$ batten policy test
policy test: 1 bundle(s), 2 passed, 0 failed        EXIT: 0

$ batten check
policy/ msrv-must-be-pinned                          EXIT: 2

The doc-shaped module passes its test and gates nothing. input.tree.tracked is documented at policy.rs:143-147 and never emitted — tree_document builds documents and missing, and nothing else. Rego makes the failure silent: iterating an undefined path yields no violations, so a dead gate and a clean tree are byte-identical. The test_ rule passes because with input as lets the author fabricate the very shape the engine cannot produce.

That is CLOUD-845, and it is a hard blocker on this row rather than tidy-up: every wave-1 agent starts from that doc, and combined with retires_with the failure mode is green tests, silent gate, deleted bash task that used to work. Ten gates migrated that way would show the census going down while enforcing nothing — the exact number this row exists to make honest.

And wave 1 is smaller than the bucket count suggests

The 20-odd tree gates were re-read by what they open, not by what they invoke. Fact::Document parses TOML, YAML, JSON, JSON5 — and Pkl, declarable-never-parsed.

reads gates migratable
structured config only 8 yes
markdown 4 no
.bats / .rs / .pkl text 5 no
no file literals 3 partly — needs the tracked list

That is CLOUD-846. Wave 1 is 8, not 22. The pilot mise-pin-agreement is in the 8 and is unaffected — both its inputs are parsed formats — so the pilot choice above stands.


BUNDLE W0 — the unblocker. Dispatch-ready now.

Both rows are unblocked, both are rules.rs / facts.rs / policy.rs / schemaone file domain, so one agent, one branch, one draft PR, per CLOUD-839's sizing. Nothing else in the campaign can start until it lands.

Superseded 2026-08-21 by the six-bundle dispatch at the foot of this row. W0's chain grew from two rows to five once the acquisition boundary was traced (CLOUD-849/850/851); it is now bundle A there. The prompt below is still accurate for the 845→846 half and is kept because bundle A's prompt builds on it.

# Chain File domain PR shape
W0 unblock-migration CLOUD-845CLOUD-846 rules.rs (tree_document), policy.rs (module doc + policy test), facts.rs, git.rs (list_tree), schema/* 1 PR

845 first: it fixes the input the doc promises and closes the false-green class. 846 then adds the lines fact on top of a tree_document that is already correct, and its §5 assertion (a finding may see a line, never carry one) is easier to state once 845's input-shape check exists.

W0 unlocks wave 1 at 8 gates. It does not unlock the other 12: those wait on 846's lines fact landing and being demonstrated, which is 846's own acceptance (d).

Dispatch prompt — one paste, self-contained

You are bundle W0 of the CLOUD-843 bash-retirement campaign in the Batten repo. Read
CLOUD-843 first: it carries the census, the bucket classification and the pilot choice.
Nothing else in the campaign can start until your PR lands.

YOUR CHAIN - one branch, one draft PR, landed in this order:
  CLOUD-845 -> CLOUD-846

CLOUD-845 first. There is a REPRODUCTION on that row - run it before you change anything,
and keep it as the test. Two modules in one bundle, one copied from policy.rs's module
doc and one written against what rules::tree_document actually builds: `batten policy
test` reports 2 passed, exit 0, while `batten check` reports only one predicate. The
doc-shaped gate is dead and its test is green.

Three parts, and the third is the one worth having:
- Emit `input.tree.tracked`. Do not just delete the doc example - a tracked-path list is
  what a whole class of these gates needs. `git::list_tree` already exists at git.rs:784
  and CLOUD-833 already uses it for bundle membership under --config-from. Bound it by
  declaration the way `documents` is bounded; an ambient walk would make the `read`
  classification a lie by degrees.
- Make policy.rs's module doc true, and assert it: every field its examples reference
  exists in what tree_document emits. Same shape spawn_census.rs:216 uses against
  clippy.toml. This defect is CLOUD-589's class recurring in the file that landed
  CLOUD-831, which was filed for exactly it - so an assertion, not a careful edit.
- `batten policy test` refuses a `with input as` naming a key the engine cannot produce,
  at exit 1 (config fault, not a policy verdict). This closes the CLASS. Without it every
  field added to the input document reopens the same hole. CLOUD-834 is making the
  document's keys the Fact variants asserted by exhaustive match - validate against that
  same table, do not build a second list.

Then CLOUD-846 on the same branch: a lines fact, `input.tree.lines[<path>]`, so a module
can decide over a .bats or .md file. Lines rather than raw text, and the reason is rule 4
rather than convenience - a module may SEE a line, a finding may never CARRY one. Assert
that; it is the clause with teeth and the one that keeps pointer-only structural. A
declared path the tree lacks is could-not-look, never an empty array. Acceptance (d) is a
demonstration, not a claim: migrate one of the four markdown gates as proof.

CROSS-BUNDLE: you are the only branch in flight on this campaign. CLOUD-834 is In Progress
in the CLOUD-839 fleet and also touches the policy input document - it projects the Fact
variants into it. Coordinate through that row rather than racing it: if 834 lands first,
rebase and validate against the table it built.

WORKFLOW CONTRACT (AGENTS.md is authoritative; this is the summary):
- Claim by hand BEFORE writing code: `mise run claim-check`, and assign yourself. The
  automation fires on the PR event, the end of the work, so waiting for it reserves nothing.
- `git fetch origin main`, short-lived branch, never author on main.
- Commit early and often. You are pre-authorized to commit and push without asking.
- Run the full `mise run verify` after EVERY commit. Local execution is free; a CI run is
  metered and the landing lease is fleet-wide.
- Open the PR as a DRAFT immediately (`gh pr create --draft`). CI does not run on drafts.
- When the chain is complete: `mise run linear-check`, then `mise run land` backgrounded.
  Do NOT ready by hand - land readies after its push. Do NOT wrap land in bespoke retry or
  pre-check logic; main advancing under you is that loop working.
- Background anything that can exceed ~2 minutes; a foreground command is killed at ~2 min.
- Move the Linear row as you move the work. Carry the lifecycle to landed-and-verified
  without stopping to report and wait.

Wave 1, after W0 lands

Eight structured-config gates, pilot first: mise-pin-agreement end to end — module, test_ rules, task and suite deleted, a #MUTANT directive declared where it has none today. The pilot converts the estimate into a measured cost per gate, and the waves are sized from that number rather than from the count. Do not batch before it.

A second thing the fixture run turned up, recorded here rather than filed because it is a one-line observation and its home is this campaign's tooling: the protected-path gate matches batten.toml by basename, so it refused writes to a fixture's batten.toml in a temp directory outside the repository — and the advertised BATTEN_GH_GUARD_BYPASS=1 did not take as an inline environment assignment. Every wave-1 agent will hand-build such a fixture. Whoever hits it should file it rather than work around it silently.


DISPATCH 2026-08-21 — six bundles, and the one lever that decides wall-clock

Gates per PR, not agents

The fleet-wide landing lease charges per land, not per gate — one branch spends CI at a time, ci p95 ≈ 701s.

batching lease acquisitions for 82 gates pure landing time
one gate per PR 82 ~20 h
one wave per PR 6 ~1.5 h

That 13× is the whole answer to "fastest", and every other choice is noise beside it. Migration is embarrassingly parallel per gate, which makes one-agent-per-gate the tempting and slowest schedule. Fan out the authoring, serialize the landing. Past ~8 concurrent PRs each extra worker adds landing time (every land forces every other branch to rebase) without removing work time, so do not dispatch 30.

Two tracks, run concurrently

The objective is 82 gate tasks and 11 hook bodies (CLOUD-312). They share almost nothing.

Track 2 has a free start: run-shape-guard is 630 lines, opens exactly one file (mise.toml), and is otherwise pure string analysis of command — which the envelope already carries. It needs no Document fact, so it is migratable now, before any capability lands. Earliest census movement available, one land. Its last two families need CLOUD-613, which is Backlog with no Ready block.

The bundles

# Bundle Rows, in order File domain Why together
A Acquisition — the long pole, gates all of track 1 CLOUD-849 → 845 → 850 → 846 → 851 rules.rs, facts.rs, policy.rs, schema/ A strict chain on one function; splitting means agents rebasing onto each other's edits to tree_document
B Ready-block gate family CLOUD-852 → 842 → 595 → 826 → 751 mise-tasks/ready-lint, tests/ready-lint.bats Five rows, one 35-line §6 block. Any split is a guaranteed conflict for zero parallelism
C Board-gate wiring CLOUD-825 the seven board gates + their invokers Disjoint from B; released is fed /dev/null and three gates have no invoker
D Hook surface CLOUD-461 → 525 hook.rs, lib.rs, doctor The two capabilities gating contract-drift and stop-guard retirement — the hook half
E Envelope content fact CLOUD-758 hook.rs, facts.rs Prospective Write/Edit content; the hook bodies reading tool_input need it. Folds into D if the fleet is cut to five
F Instruments + base ref CLOUD-844, CLOUD-720 .claude/rules/, resolve.rs Two small independents

CLOUD-852 has landed (PR #625, b405ca8) — bundle B starts at 842.

B is the highest-leverage non-obvious bundle. It is not on the objective's critical path, it is on the throughput path: every row of every later wave passes ready-lint, and that gate misread a negation, cannot check the claim it reports checking, refuses a corpus it was changed out from under, and accepts a §7 naming tests that do not exist. 82 migrations run through it is 82 chances to ship a Ready block nobody can trust.

Order

T+0 — five agents. A is 5 deep and lands last; the rest are shallow and clear the lease before A needs it. Agent 5 takes the run-shape-guard partial migration plus C.

T+1 — after A lands. One agent, one PR: the mise-pin-agreement pilot plus all 8 structured-config gates. Not the pilot alone — its purpose is the measured per-gate cost, measured just as well inside a batch of 9, for one lease instead of two.

T+2 — three parallel PRs: the 12 lines-fact gates, the git-fact gates this row's verdict clears, and the remaining hook bodies.

T+3 — the git remainder. ~10 lease acquisitions total.

What gates the schedule, and neither is on the bundle list

  1. A must land first and nothing parallelises it. Every hour it slips slips all of track 1.
  2. CLOUD-480 must land before wave 2, not after. Batching 8–24 gates per PR means one false-green module hides inside a large green diff; mutant at its current coverage cannot see it, and retires_with admits the suite deletion anyway. Batching raises the value of the anti-false-green instrument, so it comes first.
  3. Waves 1–3 have no owner. This row's Acceptance stops after the pilot. Either it grows to carry them or a sibling row does — dispatching a wave against a row that does not claim it is how work lands with nothing recording that it did.

Dispatch is BY HAND, and that is settled

create_session is refused upstream: the session-management tools carry a mandatory-approval flag — "requires explicit approval regardless of permission mode" — and bypassPermissions, an explicit permissions.allow entry and a PreToolUse allow hook are all recorded as tested and failing (#76264, #87548). mem:connector-allowlist-recovery's STOP section carries the mechanism and the tell. Do not spend a turn re-attempting it. A human opens the sessions and pastes the prompts; each bundle's rows carry full Ready blocks, so a prompt need only name the chain, the file domain and the workflow contract.

CLOUD-1163 Retire the eight small multi-program units — 59.7s, and SIX of eight land today: BOTH remaining blockers (CLOUD-1108, CLOUD-1115) disclaim blocking in their own bodies, by name

CORRECTION (2026-08-31, second pass) — the two BLOCKERS were read from this row's own table instead of from the rows they cite, and both cited rows say the opposite IN BOLD

This row's 2026-08-31 correction fixed three wrong reasons by reading the programs. It did not re-read the two BLOCKER ROWS, and that is where the remaining error was. Read each of them at head and both disclaim blocking this row, by name:

  • Unit 9 (run-shape-guard, 14.0s) is NOT blocked by CLOUD-1108. That row was re-derived 2026-08-31 against origin/main 0683ce53 and its point 3 names this row explicitly: "CLOUD-1151's build order lists this row third and says it blocks CLOUD-1163's run-shape-guard unit. On the evidence above that unit is not blocked by file-granularity — it is a whole-file retirement whose four successors are already written, and what it owes is the ledger arms and the deletion, not a ratchet change." Verified in the tree rather than inherited: policy/run-shape.rego raises V-COMMIT-STDIN-UNBOUND, V-FOREGROUND-SLEEP and V-BACKGROUND-TIMER, and policy/task-substitution.rego is tracked and opens by naming this exact family. N = K = 4. The #MISE description at run-shape-guard.sh:2 names those same three families and nothing else; the fourth is the cargo clause on the same line. There is no family left waiting, so the file is deletable WHOLE — the ratchet's one admitted disposition.

  • Unit 11 (replay, 18.9s) is NOT blocked by CLOUD-1115. That row's §8 opens: "Blockers (§8). None, in either direction. This row blocks nothing, and a blocks CLOUD-910 relation added here on 2026-08-28 was wrong and has been removed" — the identical relation, wrong once already, re-derived here a second time. Its own measurement is the argument: "nothing refuses a retirement for a missing or failing replay… replay appears in no hk.pkl step, no mise.toml task graph, no workflow and no rule row." Confirmed at head: a pattern scan of mise.toml, hk.pkl and .claude/settings.json for replay returns two PROSE comments (mise.toml:911,952) and no invocation; the only caller of replay-pointers.py is replay.sh:80 itself.

    And CLOUD-1115 is the argument FOR unit 11's deletion, not against it. A gate whose tree arm "has never run against a real gate and cannot pass", off the landing path, invoked by nothing, is a gate deciding nothing. Its disposition is the // withdrawn: arm — subjects-and-reason with no successors at all, policy/shell-retirement.rego:708 — which is the one ledger arm that has never been used (CLOUD-1176). Unit 11 is the campaign's first honest candidate for it, and it is the single largest member of this row.

Both blockers were carried on this row's TITLE-adjacent verdict rather than on the cited row's body. That is CLOUD-1166's class one level up from the three this row already corrected: not classifying a program instead of reading it, but classifying a BLOCKER instead of reading it. The reading took minutes, as it did the first three times.

The seconds are stale, and every rank moved

The 52.2s in the old title and the per-unit column below predate CLOUD-1198's corpus regeneration. Re-derived from bench/suites/RESULTS.md at origin/main:

unit members' suites was now disposition at head
11 replay 7.1 18.9 lands — // withdrawn:
6 token-bench 16.4 + token-bench-check 0.4 10.6 16.8 lands
9 run-shape-guard 13.6 + -quoting 0.4 11.8 14.0 lands — whole-file
4 mcp-allow-check 5.9 + connector-allow-guard 0.5 + -resolve 0.4 9.5 6.8 blocked (CLOUD-1251)
10 perf-compare 0.7 + perf-gate 0.2 1.0 0.9 lands
8 gh-guard 0.9 1.0 0.9 lands
7 suite-bench-check 0.9 0.7 0.9 lands
3 container-preflight 0.4 + egress-check 0.1 0.5 0.5 not migrating (a verdict)
52.2 59.7

Unit 11 went from fourth-largest to largest, and units 11 and 9 together — 32.9s, 55% of this row — were the two carried as blocked. The row is now: six units and 52.4s land today, 6.8s is genuinely blocked on CLOUD-1251, and 0.5s is not migrating.

Arm-4 evidence for the one surviving block, quoted rather than categorised: connector-allow-resolve.sh:142 is for candidate in /tmp/mcp-config-cse_*.json; do — a glob over a set discovered at runtime, under a suffix minted per session, which [[rule.external]]'s one-declared-path-per-row shape cannot express. That is CLOUD-1251 and it is real.


Why

Units 3, 4, 6, 7, 8, 9, 10 and 11 of the 83-unit partition. Eight independent deltas, 19 programs, 52.2s combined. They are one row because each is small and none glues to another; each still lands as its own PR with its own ledger arms.

unit members suites s gates #MUTANT
9 run-shape-guard, payload-field run-shape-guard 11.2, run-shape-guard-quoting 0.6 11.8 1 3
6 token-bench, token-bench-check token-bench 10.4, token-bench-check 0.2 10.6 1 1
4 connector-allow-guard, connector-allow-resolve, mcp-allow-check mcp-allow-check 8.6, +2 9.5 3 4
11 replay.sh + replay-pointers.py replay 7.1 7.1 0 0
10 perf-gate, perf-compare perf-compare 0.8, perf-gate 0.2 1.0 2 2
8 gh-guard, gh-guard-check gh-guard 1.0 1.0 1 1
7 suite-bench-check, suite-bench suite-bench-check 0.7 0.7 1 2
3 container-preflight, egress-check, gh-preflight container-preflight 0.4, egress-check 0.1 0.5 1 1

Glue, per unit (each is a $(dirname "$0")/$here resolution or a multi-subject header, neither of which any admission in policy/shell-retirement.rego can match):

  • 9run-shape-guard.sh:110 field="$here/payload-field.sh". hook-pin-check.sh:149 names payload-field literally, so it does not join.
  • 6tests/token-bench.bats:2 declares both subjects; also token-bench-check.sh:131.
  • 4connector-allow-guard.sh:59 and mcp-allow-check.sh:324connector-allow-resolve.sh. mcp-allow-check.sh:235 iterates "$(dirname "$0")"/*-guard.sh — a glob, not a named path, so it does not glue gh-guard/ready-guard.
  • 11tests/replay.bats:2 declares replay.sh and replay-pointers.py. The .py is ungoverned so it owes no arm, but SubjectFacts::died demands it actually be deleted. The only unit whose closure crosses out of the governed set.
  • 10perf-gate.sh:48 runs perf-compare. perf-pair is a mise.toml task, not a program, so it does not join.
  • 8tests/gh-guard.bats:2 declares both; also gh-guard.sh:22.
  • 7tests/suite-bench-check.bats:2 declares both. This unit owns bench/suites/RESULTS.md, the source of every number in this campaign.
  • 3container-preflight.sh:61egress-check.sh; :82gh-preflight.

The home each unit's decision lands in

A disposition is chosen before a successor is designed (CLOUD-1176). "Port it into crates/batten" is not on the list of homes: house style §2's surface is closed and §9 says consumer-specific behaviour is reconstructed through extension surfaces, never baked into the core.

unit home why
8 consumer modulepolicy/*.rego a pure argv classifier over input.call.segments; the gh lifecycle vocabulary and the task names it recommends are this repo's, so rule 1 keeps it out of a preset
9 consumer module (policy/run-shape.rego, three families already there) + existing verb for the glue payload-field.sh's successor already ships as batten payload field; the remaining cargo-substitutes-for-a-task family derives from mise.toml task bodies, which are consumer facts
10 existing verbbatten perf / batten perf pair already shipped, effect write
6 consumer modulepolicy/*.rego, two predicates token-bench-unmethodical is a document predicate over bench/tokens/RESULTS.md (input.tree.lines); token-bench-drift is a producer-written identity verdict (input.tree["tool-verdict"]). token-bench itself is a measurement task and a measurement task is not a gate
7 consumer modulepolicy/*.rego, plus a [[pattern]] row suite-bench-check is SET EQUALITY over paths — input.tree.tracked against the corpus rows in input.tree.lines — and reads no duration at all; suite-bench produces bench/suites/RESULTS.md
4 consumer module, once a DISCOVERED-path fact exists the MCP config paths are consumer-specific by construction — and external does not reach them, because it declares one path per row and these are globbed per session (see the 2026-08-31 correction)
3 keep-as-task — NOT MIGRATING proxy and token-scope probing is not a completion gate's job, and per CLOUD-1202 that is a recorded verdict rather than a fact to wait for
11 undecided — likely DELETE replay has no successor verb and CLOUD-1115 says its tree arm cannot pass as built; decide the disposition before designing anything

Which of the eight are actually reachable

  • Unblocked: unit 8 (gh-guard-check is a pure argv classifier over input.call.segments; gh-guard.sh:22 only shells to it).
  • Blocked on CLOUD-1108 — unit 9. The ratchet is file-granular and run-shape-guard is multi-family; three of four families already landed as policy/run-shape.rego, and the remaining cargo-substitutes-for-a-task family is expressible but cannot move alone.
  • CORRECTION (2026-08-30) — "benchmark/build execution" was never a blocker. This row previously listed units 6, 7, 10 and 11 as blocked because "no fact carries" a build or a suite result. Measured against the emitted surface rather than inferred: batten perf **and **batten perf pair already ship, effect write, and crates/batten/src/perf.rs builds two binaries and spawns hyperfine. §5's split is check = read and structurally incapable of spawning; enforce/exec are the spawning side, and perf sits there. So an execution is not outside the engine — it is outside check, which is a different sentence. Unit 10 is therefore unblocked, and units 6 and 7 are blocked only on their comparator's inputs, never on the execution. This also re-scopes CLOUD-1171, which generalised the same false premise.
  • Blocked, out-of-root: unit 4 (connector-allow-resolve.sh:142 globs /tmp/mcp-config-cse_*.json), unit 3 (container-preflight reads $HTTPS_PROXY and token scopes). Superseded — see the 2026-08-31 correction below. "Out-of-root" is no longer the reason for either, and it is now the wrong reason for both.

So three of eight land today — units 8, 10, and (comparator-first) 7. That is the honest count and it is why CLOUD-1151 cannot size a wave from unit count.

CORRECTION (2026-08-31) — the blocked verdicts above predate the fact surface they were judged against

Every blocked verdict in this row was written on 2026-08-29/30. Seven fact families have landed since, and the row was never re-read against them. Enumerated from the generated schema/policy-input.schema.json at 0683ce53, input.tree now carries external (CLOUD-1167), tool-verdict (CLOUD-1171), captured (CLOUD-1188), state (CLOUD-1203), commit-meta (CLOUD-1187), git-history, forge (CLOUD-1154) and stagedall Done. Re-derived per unit:

  • **Unit 4 is still blocked, but NOT because the paths are out of root — because they are DISCOVERED rather than DECLARED. **external landed and it takes exactly one path under one named root environment variable per declared row; the schema's own description is "the parsed node of the file found at path beneath the directory the named root environment variable holds", keyed by the declaring row's id. connector-allow-resolve.sh:142 globs /tmp/mcp-config-cse_*.jsona set discovered at runtime, which no declared single path can express, and a consumer cannot enumerate the ids in advance because the suffix is minted per session. That is a real gap with no owning row; it is filed separately and this unit is blockedBy it.

  • **Unit 3 is still blocked, and its reason belongs to a different class entirely. **container-preflight reads $HTTPS_PROXY's VALUE and probes egress and token scopes. external reads a FILE under a root variable, never a variable's value, and nothing on the surface performs a probe. Per CLOUD-1202's decision, this is the scope reminder's "not a reference monitor" clause rather than a missing fact: **the disposition is keep-as-task, and the unit should be recorded as not migrating rather than carried as blocked forever. **CLOUD-1201 reaches the same verdict from the caller's side.

  • Units 6 and 7 — SETTLED, and the premise was wrong in BOTH directions. NEITHER COMPARATOR COMPARES A MEASUREMENT. An earlier revision of this clause said they compare "MEASUREMENTS against a baseline" and sent an implementer to settle which fact carries one. That was asserted from the category "comparator over two records" without reading either program, and it is the same error one level down as the two above.

    Unit 7 (suite-bench-check) reads no duration at all, and its own header says so as a design decision: "It is NOT byte-diffed against a fresh run … Wall clock is not [deterministic] … WHAT IS DETERMINISTIC IS MEMBERSHIP, and that is what rots." The predicate is set equality between two sets of PATHSgit ls-files 'tests/*.bats' against the paths in the third column of bench/suites/RESULTS.md, both directions. Both are on the surface today: input.tree.tracked ("repository-relative paths the working-tree walk yields — paths, never content") and input.tree.lines over the corpus. Unit 7 needs no new fact and is fully expressible now. Its output is already pointer-only by the same rule 4 the module inherits — "Never a duration — a number here would be a second authority over the corpus."

    **Unit 7 does need a **[[pattern]] row, and this is the one real hazard: the corpus row is parsed for a backticked path inside a Markdown table, and the shell records that exact regex being got wrong once — "Run against a formatted corpus it matched no row at all and reported every tracked suite as missing: 150 findings, all false, from a gate that looked like it was working." prettier owns Markdown here and pads table columns. Carry that case into the successor's suite.

    Unit 6 (token-bench-check) is two predicates, and neither is a threshold either. token-bench-unmethodical asks whether every published figure states workload, baseline, run count and method — a pure document predicate over bench/tokens/RESULTS.md, so input.tree.lines. token-bench-drift asks whether the committed table is byte-identical to what a fresh run produces from committed fixtures — an identity check, not a comparison against a threshold, and exactly the shape input.tree["tool-verdict"] landed for: *"read back from a record a producer wrote OUTSIDE the engine, because *check is read-only and structurally cannot run a validator — KEYED BY (tool, pinned version, input digest)". The producer runs the generator; the module reads the verdict. **So unit 6 is also expressible, and **tool-verdict IS the answer for its drift half — the opposite of what this clause previously said.

    Neither unit needs records or produced, and neither needs a measurement fact that does not exist.

**THE COUNT CHANGES: it is now FOUR of eight, not three. **(Superseded 2026-08-31 by the second-pass correction at the top: it is SIX of eight. Units 9 and 11 were blocked on rows that each disclaim blocking.) Unit 6 was carried as blocked on a comparator input it does not need, and reading token-bench-check.sh rather than its category shows it is expressible today. Units 6, 7, 8 and 10 land now. Unit 3 is not migrating (a verdict, not a block); units 4, 9 and 11 remain blocked, on CLOUD-1251, CLOUD-1108 and CLOUD-1115 ~~respectively. ~~Only unit 4 remains blocked, on CLOUD-1251. CLOUD-1108 and CLOUD-1115 each disclaim blocking this row in their own bodies — see the second-pass correction at the top — and both blockedBy relations have been removed.

Every wrong reason in this row came from classifying a program instead of reading it. Three corrections, three instances: "out-of-root" for unit 4, "missing fact" for unit 3, "comparator inputs" for units 6 and 7. The reading in each case took minutes. Read the program before recording why it cannot move.

Two need a runnable successor at a real path, not just a ledger arm

run-shape-guard.sh is .claude/settings.json:24's hook command; serena-mcp.sh (a different unit) is .mcp.json:4's. Deleting either without an executable at that path disarms a live hook silently. Both consumer files are ungoverned, so the repoint is free — but it must happen in the same delta.


Refinement — Ready (eight deltas, one row; units 8, 10 and 7 land today)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Authority boundary (§1). 19 programs and 13 suites across eight PRs. One // carried: arm per deleted path; replay-pointers.py is deleted without an arm (ungoverned) but must be gone. Ten $MUTANT_GATES entries and 14 #MUTANT rows move to successors' tiers.
  • Computable predicate (§2). Each unit's decision is conserved; this row moves where it lives, never what it concludes. Conserve the decision, not the defect (CLOUD-1176): where a unit's shell carries a tracked defect, the successor implements the corrected decision and the defect's own row records the change — a retirement that launders a known defect forward into Rust is the failure this campaign exists to avoid, not evidence of fidelity. What must be shown unchanged is the verdict over every case the dying suite covered, minus the cases a tracked defect row explicitly re-decides.
  • Deliberately not in scope (§2). Deciding CLOUD-1108's file-granularity fix. Changing what any benchmark measures. Merging any two of these units — they are independent and merging them would manufacture the glue this partition exists to avoid.
  • **Effect (§3). **read for the guards and checks. token-bench, suite-bench, perf-* and replay execute suites or builds and keep their existing effect class.
  • Output and exit (§5). Pointer-only per unit. Exit follows the 0/1/2/3 table; could-not-look is 3, never a false 2.
  • **Commit / bump (§6). **refactor(ci)no bump, per PR. Below 0.1.0 every release-worthy type collapses to a patch, but refactor is not one: it releases nothing at any version. CLOUD-595's correction.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/; no .bats file is added or edited (V-SHELL-RULE-ADDED refuses one at deny). One arm per deleted path (CLOUD-908). Shown able to fail per CLOUD-418 per unit, with the anti-vacuity mirror each time. For unit 9 specifically: the hook must still deny after the repoint — a test that only checks the module loads would pass over a disarmed .claude/settings.json. Mutated: 14 rows re-homed, mutant-census green across every move. Replayed: per unit; CLOUD-1115 is the standing caveat on replay's tree arm, and unit 11 **is **replay — it cannot be its own instrument.
  • **Blockers (§8). **blockedBy CLOUD-1251 (unit 4 only). ~~blockedBy ~~CLOUD-1108 (unit 9 only). Removed 2026-08-31: CLOUD-1108's own re-derivation names this row's unit 9 as not blocked by it, and CLOUD-1115 states it blocks nothing. relatedTo CLOUD-1151 (the wave owner), CLOUD-1115, CLOUD-908, CLOUD-418.

Acceptance

  • Eight PRs, 19 programs and 13 suites deleted, one arm per deleted path, replay-pointers.py gone.
  • .claude/settings.json:24 points at a live executable and the hook still denies, asserted end to end.
  • bench/suites/RESULTS.md survives unit 7's retirement — the campaign's own measurement source must not die with its producer.
  • mutant-census green; all 14 mutations honoured.
  • Each unit records whether it landed or why it could not.

Units 3, 4, 6, 7, 8, 9, 10, 11 of 83.

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**
it is not a content problem. Nobody claims a 66-unit row. Over one session four bundles
(A–D) were dispatched and two more were sized, and every one of them routed around this

**row — not because its members are blocked, but because there is no way to pull a slice of
it without first re-deriving the slice.

Measured: ranking every suite whose subject IS a mise-tasks/ program and which no
dispatched bundle claims gives 398.1s in the top 25 alone, and the overwhelming majority
of those rows are this one's members.

The missing deliverable is a PARTITION into claimable waves — each wave a size one PR can
carry, each member with its home and its verdict, so a next agent pulls wave N without
re-deriving anything. This row's own §1 says "up to 66 PRs, one per unit", which is the
unit of WORK; what it lacks is the unit of CLAIM. That is the same distinction
CLOUD-1174 drew and whose artifact was never built.

main-watch — 34.4s, and it was written off four times on one word

Recorded here because it is this row's member and its verdict has never been read from the
program. Four consecutive bundle tables parked it as "a poll" and moved on.

mise-tasks/main-watch.sh85 lines, 0 #MUTANT, tests/main-watch.bats 34.4s.
Its own #MISE description:

Block until origin/main advances past a given SHA (conditional poll — a quiet main costs no rate limit)

That is CLOUD-1144's waiter class, not a gate: the predicate moves and
the poll loop stays outside. And the class has a landed precedent — ci-wait retired
under exactly that disposition and is one of the five programs
CLOUD-1253's control set confirms really went.

One constraint decides whether it lands cleanly, and it is why this is not a free 34.4s:
main-watch is invoked by name from the landing pathland runs ci-wait alongside it
and whichever answers first decides the lap. It must retire behind a [tasks.main-watch]
wrapper in mise.toml so land.sh is never edited, and mise run land must still work with
land.sh unmodified. [tasks."checks-green"] (mise.toml:1983) and [tasks."ci-wait"]
(:2031) are the two landed precedents for that shape.

Verify the wrapper before dispatching it, not after. It is the one member of this row
whose failure mode is a wedged landing loop rather than a red check.

hook-latency-drift now has a row

CLOUD-1270 — 24.6s, 2.2%, and it had fallen
between every table exactly as signing-posture did below. It is a reporter on a clock,
not a gate ("Report (never gate) … a question about the world, so it runs on a clock"), so
its disposition is one of three and none is assumed. Same failure as the correction below
records: excluded from a bundle on a one-line category rather than a reading.


Why

The tail of the 83-unit partition: 66 single-program units, 251.3s combined. Each is its own delta — two deleted paths, two // carried: arms, no glue to anything. They are one row because none needs a design decision; each needs a successor written.

This row exists so no governed program is unaccounted for. CLOUD-843 and CLOUD-910 each stopped short of that, and it is why the campaign has retired one program.

EACH UNIT NAMES A HOME (2026-08-30), and it is not "a successor in crates/batten"

§1 below says each PR *"lands a successor in *crates/batten/ and/or policy/*.rego". CLOUD-1176 makes the home the first question, and a scan of the members says the answer is rarely the core:

  • PRIOR ART (§9) — the whole "third-party exec" row below: renovate-config-validator, pkl-check, mutant, hook-profile-check, macos-link-check, perf-*. These wrap a tool already on PATH. The wrapper deletes; a command rule names the tool.
  • CONSUMER MODULE (rule 1) — everything in the forge/board row that decides over CLOUD-* keys or this repo's workflow states: linear-check, ready-cites-check, done-check, release-backfill, bot-issue.
  • PRESET — the generic tree-shape checks in the unblocked list: awk-regex-check, pipefail-grep-check, timeout-check, no-doctests, shebang-adjacent hygiene. None names a Button identifier.
  • EXISTING §2 VERBschema-check, config-deprecations, skill-check shell to batten itself; their home is generate/config, not a new verb.
  • DELETE — candidates that guard nothing once their home is examined, spelled by CLOUD-1080's withdrawal arm.

The per-program home column is NOT written here — hand-listing 66 homes in an issue body is the stale-list defect CLOUD-1166 measured. But the table this clause defers to DOES NOT EXIST (corrected 2026-08-31): CLOUD-1174 is Done and no derived unit table is tracked — git ls-files finds none, and the only reference to it in the tree is policy/suite-subject-retirable.rego:132 citing its principle. So each PR decides its own home from CLOUD-1176's five and records it, until somebody builds the table. Deferring a deliverable to an unbuilt artifact on a closed row is CLOUD-1253's class.

Unblocked — predicate expressible over committed tree content today

These need only a successor written. ~30 programs.

ci-tools-check 2.0 · ci-slow-needed 2.0 · suite-select 2.3 · awk-regex-check 1.8 · memories-check 1.7 · tree-clean 1.7 · mutant-census 1.7 · install-check 1.5 · module-map-check 1.0 · mcp-timeout-budget 0.9 · hook-matcher-check 0.8 · stop-posture-check 0.8 · msrv-pin-agreement 0.6 · publish-credential-check 0.6 · pipefail-grep-check 0.6 · digest-major-agreement 0.6 · batten-glob-check 0.5 · hook-pin-check 0.5 · cap-drift 0.5 · no-doctests 0.4 · timeout-check 0.4 · license-table-check 0.4 · report-only-check 0.4 · coderabbit-config-check 0.4 · mise-action-floor 0.3 · rust-paths-check 0.3 · rules-drift 3.1 (see CLOUD-1150) · verified 0.8 · lock-complete 6.1 · release-tracking-check (see below).

Two verdicts worth stating because a naive scan says otherwise:

  • tree-clean reads git status --porcelaininput.tree.git-status carries changed[] and uncommitted. Expressible.
  • verified reads .git/batten-receipts/{verify,linear-check}.$headFact::Produced / input.tree.produced is exactly that keyed read-back. Expressible.

CORRECTION (2026-08-31) — FIVE OF THE SIX BLOCKED CLASSES BELOW HAVE HAD THEIR PRECONDITION LAND, AND THE TABLE WAS NEVER RE-READ

This row is In Progress with a live PR. The table immediately below is the one a**
working session is reading, and it was written on 2026-08-30 against a fact surface
**that no longer exists. Enumerated from the generated schema/policy-input.schema.json
at origin/main 0683ce53:

class in the table below its precondition status
forge/board (16 programs) CLOUD-1154 DONEinput.tree.forge exists
out-of-root (4 programs) CLOUD-1167 DONEinput.tree.external exists
third-party exec (10 programs) CLOUD-1171 CLEARS THE WRAPPERS, NOT ALL TENinput.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 DONEinput.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:135 names a DECLARED path under ${HOME}
    and is expressible via external today. transcript-corpus-check:97 reads
    $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. Read serena-mcp and doctor-check before 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 at origin/main 0683ce53, finding-sink-check is a per-turn join with a negated arm: prose matching a path:line citation, joined against the same turn's tool_use records, each classed against the issue-read-check receipt'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:157 extracts get_issue payloads, which is further still from a count.

    The recorded reason was "input.call.transcript is 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:

  • :98cargo metadata --locked --format-version 1. A third-party tool over Cargo.lock, which is exactly the (tool, pinned version, input digest) key tool-verdict is built on.
  • :83–:89BATTEN_EVALUATOR_METADATA already 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 Doneinput.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 recordcrates/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, perfone 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/batten verb. With **one **// carried: arm per deleted path, or CLOUD-1080's withdrawal arm where no successor is owed, and its $MUTANT_GATES entry and #MUTANT rows re-homed. mise.toml, hk.pkl and .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). **read for 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/3 table, could-not-look is 3.

  • **Commit / bump (§6). **refactor(ci)no bump, per PR. Below 0.1.0 every release-worthy type collapses to a patch, but refactor is not one: it releases nothing at any version. CLOUD-595's correction.

  • Test obligation (§7). Over the compiled binary in crates/batten/tests/; **no **.bats file 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 #MUTANT row re-homed, mutant-census green after each PR, never only at the end.

  • **Blockers (§8). **blockedBy CLOUD-1154 and CLOUD-1156 for the members named above; the ~30 unblocked members are dispatchable now. relatedTo CLOUD-1151 (the wave owner), CLOUD-908, CLOUD-418.

  • Weakens (§8) — the config-deprecations member only, and §1's "every repoint is free" is wrong for it. §1 names mise.toml, hk.pkl and .github/workflows/** as the invoking surfaces and calls them ungoverned. That holds for the other members; it is false for config-deprecations, which is invoked from a [[rule]] row in batten.toml — a protected path, judged by config-lint against origin/main. Retiring the program forces that row's glob and check onto the successor, and config-lint reads 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 under test:cargo on every verify and every CI run. The deprecation predicate itself does not move: batten config deprecations decides it before and after, and the baseline is still the newest release tag in version order.

    Weakens: rule-predicate-changed at rule[no-key-leaves-the-schema-unannounced].check

    Weakens: rule-predicate-changed at rule[no-key-leaves-the-schema-unannounced].glob

    Recorded honestly: this clause was groomed after the work started, on 2026-08-31, once config-lint refused PR refactor(ci): retire config-deprecations, reference-check, schema-check and skill-check #780. batten.toml:274-288 records the same gate blocking the same smell for CLOUD-437, where the resolution was to back the change out instead. The alternative here — drop config-deprecations from 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/batten verb.
  • 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-census green after every PR.
  • bench/suites/RESULTS.md regenerates and the recovered seconds are reported against the 251.3s this row claims.

Units 14–83 of 83, minus those filed separately.

CLOUD-1150 `.claude/rules/toolchain.md` says one sibling-edit admission is allowed where the module carries three, and `rules-drift` cannot see a restated rule-NAME set

Why

.claude/rules/toolchain.md:69-73 — the doctrine every session is routed to before touching a mise-tasks/*.sh or a tests/**/*.bats — says:

One edit is admitted, and only one, so it is not rediscovered as an exception: only_drops_a_retired_reference — a sibling file dropping a declaration row that names a path this same change retires, adding nothing.

policy/shell-retirement.rego carries three admissions: only_drops_a_retired_reference (:190), truncates_a_retired_reference (:269, added by CLOUD-1051) and repoints_at_the_declared_successor (:255, added by CLOUD-1121). The file is two behind, and the sentence does not merely omit them — it asserts a closed count, "and only one", and pre-empts correction with "so it is not rediscovered as an exception."

A stale parenthetical is a typo. A stale closed count that forbids looking further is a false premise delivered with the authority of the rule. That is rules-drift's own charter sentence, and this claim is inside its stated scope and outside its reach.

Measured cost, in this repository, this week

A grooming session read that sentence, concluded three governed programs were permanently unretirable, and wrote that conclusion into two dispatched agent prompts and five issue bodies (CLOUD-1100, CLOUD-761, CLOUD-1092, CLOUD-1113, CLOUD-1111). CLOUD-1100's scope was cut on it, with the row recording *"the program stays in the tree, dormant behind the compiled authority, and that is the honest state rather than a shim." *CLOUD-761's consumer half was blocked on it outright. Both dispatched sessions were running by the time it was caught and could not be recalled.

The correction that followed was also wrong, in the other direction — see CLOUD-1149. Three passes over one sentence, two of them landing on the board.

The mechanism half, and its coupling

rules-drift already owns this class. Its header: "Gate: a value .claude/rules/*.md restates still agrees with the mechanism that owns it (CLOUD-506)", and its stated bound is the sharp half:

"WHAT THIS MUST NOT DO... demand that a value be restated. The file's rule is the opposite... This fails a claim that is PRESENT AND WRONG, never one that is absent."

"One edit is admitted, and only one" is present and wrong. It is not an absent-value case, so gating it does not invert the file's own anti-restatement discipline. The gap is only in shape: rules-drift's anchors are value-shaped (`VAR` (N)), and this is a rule-name set restated with a closed count.

mise-tasks/rules-drift.sh is itself governed, so extending it is not an edit — it is a retirement, or it is nothing. The census classes it FREE (272 lines, tests/rules-drift.bats 3.1s, in $MUTANT_GATES, 1 #MUTANT, no inbound governed reference), so the retirement is among the cheapest available. This row must not assume an in-place edit; the two landable shapes apply to it exactly as to everything else.

The prose half is landing separately and immediately

.claude/rules/toolchain.md is ungoverned. The corrected paragraph — three admissions named with a pointer to the module rather than paraphrased bodies, the literal-path-versus-relative-resolution discriminator from CLOUD-1149, and the from-trunk-blob-fetch pattern that is a genuine blocker — is being landed now, ahead of this row, because every session reading the file today reads the false premise. This row owns the gate, not the sentence.


Refinement — Ready (gate the restated rule-name set; retire rules-drift to do it)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Authority boundary (§1). The successor to mise-tasks/rules-drift.sh, in crates/batten/ or as a policy/*.rego module. The program and tests/rules-drift.bats are deleted, two paths and two // carried: ledger arms, and the $MUTANT_GATES entry moves with it. .claude/rules/*.md and .serena/memories/** stay the surfaces read; the existing value-shaped predicates are conserved unchanged.
  • Computable predicate (§2). A .claude/rules/*.md sentence that names a set of identifiers the mechanism owns — and asserts a closed count over them — disagrees with the mechanism when the counts or the names differ. The identifier set comes from the module or source that defines them, never from a second list.
  • Do not invert the file's own discipline (§2). Conserved from rules-drift.sh's header and load-bearing: this fails a claim that is present and wrong, never one that is absent. Prose stays free to name a rule without being made to enumerate its siblings. A gate pushing toward completeness would be the defect, not the fix.
  • Deliberately not in scope (§2). Deciding CLOUD-1149's admission shape — this row gates the claim about the admission set, whatever that set turns out to be. Widening to prose surfaces beyond .claude/rules/** and .serena/memories/**. Gating AGENTS.md, which is a different surface with a different rule.
  • **Effect (§3). **read. It reads committed text and decides.
  • Output and exit (§5). Pointer-only: path:line, the name, and the two counts — never the sentence, since a rules file quotes command lines and env names. Conserved from the existing gate. Exit 0/1 with no exit 2: it reads only committed text, so there is no could-not-look state — also conserved, and stated so the port does not invent one.
  • **Commit / bump (§6). **refactor(ci)no bump. Below 0.1.0 every release-worthy type collapses to a patch, but refactor is not one: it releases nothing at any version. That is CLOUD-595's correction.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/; no .bats file is added or edited, since V-SHELL-RULE-ADDED refuses a new one at deny. Shown able to fail per CLOUD-418: the exact toolchain.md sentence at its pre-fix bytes must be reported, and the corrected sentence must pass — the second is the anti-vacuity half, without which the first is satisfied by a gate that fails every rules file. A third case: a sentence naming one rule without claiming a count is not reported, which is the discipline clause above made observable. **Mutated: **rules-drift declares #MUTANT restated-default-drifts; that mutation and the $MUTANT_GATES entry move to the successor's tier and mutant-census stays green across the move. Replayed: old and new run over the same .claude/rules/** tree and must agree finding-for-finding.
  • Blockers (§8). None. relatedTo CLOUD-506 (which created this gate for the identical failure one shape down), CLOUD-770 (which widened it to the memory surface), CLOUD-1119 (three stale instruction-surface claims), CLOUD-1132 (the two-shapes doctrine surface), CLOUD-1149 (the admission clause this claim is about).

Acceptance

  • The corrected toolchain.md paragraph passes; its pre-fix bytes are reported.
  • A rules sentence naming one mechanism without a closed count is not reported.
  • mise-tasks/rules-drift.sh and tests/rules-drift.bats are deleted with one ledger arm each, and no other governed file is edited.
  • mutant-census green, #MUTANT restated-default-drifts honoured at its new home.
  • The existing value-shaped findings are unchanged, replayed against the same tree.

Found because the sentence this row is about caused two dispatched agent sessions and five issue bodies to be written on a false premise, and the gate that exists to catch exactly this could not see it.

CLOUD-1194 Retire the ten board gates into rule ids — all three preconditions have LANDED; this is the family that decides whether the campaign adds ~25 commands or none

SIZED, 2026-08-31 — and a dispatched bundle is carrying the WRONG ROSTER for this family

This row has never carried its own sizing. Measured at origin/main against
bench/suites/RESULTS.md, over the eleven programs this row's own body enumerates:

program s lines #MUTANT program s lines #MUTANT
graph-check 21.6 928 13 spec-ref-check 5.3 198 2
ready-lint 8.1 729 4 claim-race-check 4.6 142 1
ready-cites-check 6.2 332 4 landed-check 2.8 277 4
closing-key-check 2.5 308 3 claimed-keys 2.4 212 3
done-check 1.2 140 1 done-pr-check 0.9 140 5
duplicate-close-check 0.8 154 3 total 56.4 3,560 43

**The 43 mutation rows are the cost to size against, not the 3,560 lines. **graph-check
alone carries 13.

A dispatched prompt has the wrong members — correct it before it lands

A "Bundle G" prompt handed out 2026-08-31 lists this family as ten programs, 74.6s, 3,718**
**lines, 48 #MUTANT. Every one of those figures is wrong, because the roster was built from
board-sweep.sh's composition list at :229/:279 rather than from this row's own body.
It is wrong in both directions:

  • Wrongly includedboard-sweep 6.9, released 16.9, in-progress-drain 5.1
    (28.9s). These are the composer and two gates it drives; they are CLOUD-825's grouping,
    not this row's declared membership.
  • Wrongly excludedclosing-key-check 2.5, claim-race-check 4.6, claimed-keys 2.4,
    done-check 1.2 (10.7s). All four are named in the "Why" section below.

That is CLOUD-1166 arm 3 — a membership list taken from somewhere other than
its owner and mutated in the copy — committed into a dispatch prompt, which is the
destination that row now records as the least recoverable one. The roster above is the**
**authority. Cite it; do not restate it.

Two exclusions that ARE correct, with their reasons

  • board-payloads (0.4s) — stranded. tests/remedy-payload-source.bats:2 subjects it
    alongside batten.toml, an immortal subject, so no landable retirement exists
    (CLOUD-1156).
  • board-diff-overlap (24.1s) — needs to extract PATHS from an issue body, and
    input.tree.captured is a declared REDUCTION (present, count, or a bounded token), never a
    payload. CLOUD-1162, genuinely open.

And mutant cannot reach any successor this row lands

mise-tasks/mutant.sh:158 hardcodes suite="tests/$gate.bats" while :157 falls back to
policy/$gate.rego for the SOURCE — so a mutation can be applied to a Rego module and no
suite exists that could turn red. 32 of 32 modules carry #MUTANT-EXEMPT for exactly this.
Each of the 43 rows re-homed here takes an exemption naming
CLOUD-1267 until that row lands. Do not invent a new exemption reason, and do
not read the resulting green as coverage.


THE THIRD BLOCKER LANDED TOO — 2026-08-31. This row is dispatchable.

The re-derivation below records CLOUD-1188 and CLOUD-1228 as Done and
CLOUD-1182 as "the one genuine blocker left". It has since landed, checkable at
origin/main 5b98174:

git grep -l 'V-SUCCESSOR-KIND-UNDECLARED' -- policy/    # policy/shell-retirement.rego
git log --oneline -- policy/shell-retirement.rego       # 34d2640 refactor(ci): declare
                                                        #   kind:mechanism on the eight
                                                        #   retirement arms

kind:verb / kind:mechanism is a declared field on every engine-source arm and the
refusal exists. All three blockedBy relations have been removed and the title no
longer says "once they have a fact channel" — the channel exists, and so does the
kind declaration that was meant to stop this family taking the verb route by default.

A correction block does not correct a title, and the title is what a queue listing
renders — which is why this row read as waiting on a capability for as long as it did.

What is genuinely still owed, and it is not a precondition

Two per-gate decisions this row already names, neither of which blocks starting:

  • landed-check.sh:199 reads git log --format='%B' origin/main — a message BODY,
    which rule 4 refuses on the policy input. Restate it against trailers or subjects
    (input.tree["commit-meta"] carries both since CLOUD-1187), or record that it does
    not move. Stated, never dropped, as this row's own acceptance requires.
  • graph-check.sh:163's constructed-sibling spawn of ready-lint.sh must disappear
    rather than be reproduced.

Nothing else in the body below is changed by this note.


Why

.claude/rules/toolchain.md:169-171 names this family: "each is a pure function of stdin (get_issue payloads piped in by the caller, since no tracker credential exists)".

  • graph-check.sh, landed-check.sh, closing-key-check.sh, spec-ref-check.sh, done-check.sh
  • claim-race-check.sh, done-pr-check.sh, ready-cites-check.sh, duplicate-close-check.sh
  • plus the already-ported ready lint and lint brief / design audit, whose inputs are the same shape

Ten-plus pure predicates that are CLI verbs only because they have nowhere to read from. In a 20-gate sample of the remaining campaign, 10 needed a verb and six were this family — so this is the single largest determinant of whether the surface grows.

With CLOUD-1188's capture-store fact they are all policy modules costing the surface nothing.

RE-DERIVED 2026-08-31 against origin/main 0683ce53 — TWO OF THIS ROW'S THREE BLOCKERS ARE DONE

The last line of this body still reads "Blocked by CLOUD-1188 for the fact channel these predicates read." That channel landed.

blockedBy state evidence
CLOUD-1188 — the capture-store fact DONE 2026-08-31, PR #773 input.tree.captured is on schema/policy-input.schema.json: "Declared id -> the REDUCTION that row asked for … Resolved from the capture store and NEVER from stdin"
CLOUD-1228 — a successor told which repository supplies its config DONE 2026-08-31 14:20, PR #778 this was the deeper one: 50 of the corpus's suites git init a scratch repo, which removes the committed config every compiled successor reads. Its own acceptance is "CLOUD-1221's retirement lands with tests/graph-check.bats unedited and green" — the fidelity argument this whole family depends on
CLOUD-1182 — the successor-KIND declaration OPEN — In Progress since 2026-08-31 15:25, PR #789 the one genuine blocker left, and the one that decides whether these ten become rule ids or ten more nouns

So this row is one merge from dispatchable, and the sequencing matters for whoever picks it up: CLOUD-1182 must land FIRST rather than alongside, because it is the mechanism that stops this family taking the verb route by default — which is the exact outcome this row's title says it exists to prevent.

Every program in the family is still tracked, and every pointer this row cites still resolves

Read at origin/main 0683ce53. Nothing has retired, so no count here has moved:

ready-lint.sh 729 · graph-check.sh 928 · ready-cites-check.sh 332 · closing-key-check.sh 308 · landed-check.sh 277 · claimed-keys.sh 212 · spec-ref-check.sh 198 · duplicate-close-check.sh 154 · claim-race-check.sh 142 · done-check.sh 140 · done-pr-check.sh 140 lines.

  • The graph-check spawn is real and at the line this row names. mise-tasks/graph-check.sh:163 is lint="$(dirname "$0")/ready-lint.sh" — the constructed-sibling spelling, which is why shell-retirement names it by hand.
  • The commit-body read is real, and so is the second spawn. mise-tasks/landed-check.sh:199 is log=$(git log --format='%B' origin/main 2>/dev/null || true), and :215 pipes that log into "$here/claimed-keys.sh" --closing-only. Both halves of this row's "extra baggage" bullet hold.
  • ready-lint.sh's 729 lines confirm CLOUD-1221's count exactly — it is a second implementation, not the shim three rows record it as, and it is still standing.

What is NOT re-derived: the "20-gate sample … 10 needed a verb" figure and the .claude/rules/toolchain.md:169-171 line pointer are inherited from 2026-08-30 and were not re-measured. The family membership was.

Two of them carry extra baggage

  • graph-check.sh also spawns ready-lint.sh (:163) — the line shell-retirement.rego:244 names by hand as the repointing case. The spawn disappears once both are rules; that is a simplification the port must actually take rather than preserve.
  • landed-check.sh and closing-key-check.sh also spawn claimed-keys.sh, and landed-check.sh reads git log --format='%B' origin/maina message body, which rule 4 refuses on the policy input (git.rs:3016-3018). That predicate needs restating against trailers or subjects, or it does not migrate. Settle it per gate, and record what does not move.

Refinement — Ready (port the board family)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • **Authority boundary (§1). **policy/*.rego, batten.toml rows, crates/batten/src/ for any surface removal, crates/batten/tests/. Predecessor programs and suites are DELETED.
  • Computable predicate (§2). Each gate decides from the capture-store board fact plus tree/git facts, behaviour-identical to its predecessor, invocable as batten check <rule-id>.
  • An unpopulated store is could-not-look, never a pass (§2). A board gate reporting green because nobody filled the store is the vacuous pass in its purest form — the input.tree.missing discipline applies and must be tested per gate, not assumed from CLOUD-1188.
  • Deliberately not in scope (§2). Any network fetch of policy at runtime — CLOUD-129's verdict stands — and it is scoped, which this clause used to hide. Its adopt table rejects "Remote policy distribution — OCI pull, registry resolution, any network fetch of policy at runtime" on the ground that it is "a supply-chain surface". It does not reject a gate fetching the DATA it decides over; read that way it would forbid every forge and tracker read this repository already performs, and claim-not-raced — which reaches GitHub on the tree scope — would be refused by a verdict it was written after. Corrected 2026-08-31 because the unscoped wording here was quoted back as a blanket ban and used to refuse CLOUD-1260, which fetches a tracker row and no policy at all. That is CLOUD-1166's class: a scoped decision restated without its scope becomes a wider decision nobody made. Changing what any gate decides. The self-derivation family (derived, schema, skill, reference), which is a different shape and its own row.
  • **Effect (§3). **read.
  • Output and exit (§5). Pointer-only: <issue-id>:<line> <rule>, never issue prosesurface.rs:2101-2103 states why, and it is the reason CLOUD-1188 projects reduced tokens rather than payloads.
  • **Commit / bump (§6). **feat(policy)! per wave.
  • Test obligation (§7). Per module: test_ rules plus a compiled-binary tier. Each predecessor's existing .bats cases are the fidelity corpus before deletion. Shown able to fail per CLOUD-418, including the could-not-look case above.
  • [rule.conserves] arms declaring successor kind (§7) per CLOUD-1182; $MUTANT_GATES shrinks with each retirement.
  • **Blockers (§8). **blockedBy CLOUD-1188 (satisfied — Done, PR feat(facts): the missing fact families — one commit per row #773), CLOUD-1228 (satisfied — Done, PR refactor(toolchain): the surviving Ready gate is a second implementation, not a shim #778) and CLOUD-1182, the one still open and the one that must land first. The two satisfied relations are kept rather than removed, because the record of which precondition each half of this family waited on is what the re-derivation above is for. relatedTo CLOUD-1183.

Acceptance

  • Every gate in the family is a [[rule]] row; none has a SURFACE path.
  • Each is behaviour-identical against its predecessor's own corpus.
  • An unpopulated capture store yields could-not-look, per gate.
  • Any predicate needing a commit body is restated or recorded as not migrating — stated, never dropped.
  • graph-check's spawn of ready-lint is gone rather than reproduced.

Filed for the fact channel these predicates read. That channel is CLOUD-1188's and it has landed; what still gates this row is CLOUD-1182, per the re-derivation above.

CLOUD-1049 An unparseable declared source reaches no policy module and reports no cause, so `input.tree.missing` is empty where a module's own test proves it populated

Why

policy/privileged-lane.rego opens with a clause whose comment states the contract:

A file this build could not parse lands in input.tree.missing rather than in documents (CLOUD-845). Without this clause a workflow that fails to parse is simply absent from every rule below and the module reports GREEN over a file it never read — a vacuous pass.

That clause does not fire against the engine. An unparseable declared source produces no violation, no finding, and no reported cause — not even under --strictness strict.

Reproduction, run 2026-08-25 at v0.0.113

A throwaway repo, one row, the module copied in, and one workflow that is genuinely invalid YAML (confirmed independently — yaml.safe_load raises ParserError):

batten.toml:  [[rule]] id = "privileged-lane-tests-origin", kind = "policy",
              scope = "tree", sources = [".github/workflows/*.yml"],
              module = "policy/privileged-lane.rego", severity = "deny"

.github/workflows/auto-bot-land.yml:
    on: [workflow_run
    jobs: : : not yaml at all
        - "
$ batten check                      exit 0
$ batten check -J                   {"fail_on_warning": false, "findings": []}
$ batten check --strictness strict  exit 0

Committed or untracked, the answer is the same. The same fixture with a PARSEABLE workflow that fails the predicate denies correctly at exit 2, so the row is live and selected — it is specifically the unparseable path that vanishes.

The module's own test_an_unparseable_workflow_denies_rather_than_passing passes, because with input as hands itself a populated missing. That is CLOUD-845's class exactly: a with input as test cannot tell a populated channel from one nothing ever fills.

Why this is not CLOUD-874

CLOUD-874 is a collision: a path declared as both lines and documents takes the miss arm and is reported Absent. This fixture declares one row and no lines at all, so there is nothing to collide with — a different cause reaching a nearby symptom. What this row adds that CLOUD-874 explicitly lacks is that it is reproduced by a running fixture; that row records "Confirmed by reading those three; not reproduced by a running fixture."

Why it matters more than one module

Every tree-scoped module the retirement campaign migrates inherits this. A migrated gate whose input will not parse reports the same green as a clean tree, which is the failure missing was introduced to close — and the campaign's whole value proposition is that a migrated gate answers identically to the bash task it replaced. A bash task reading a broken YAML file fails loudly.


Refinement — Ready

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Source of truth (§1). crates/batten/src/rules.rsacquire_declared, project_declared, tree_document and the Acquired enum. Nothing outside that file decides what a declared path projects to, and documents_acquired is the counter any fix must leave meaning what it means today.
  • Computable predicate (§2). A fixture declaring one unparseable document under sources makes its module's input.tree.missing clause fire, at the row's severity. Decidable from the working tree; no network, no spawn.
  • Effect (§3). read. No verb is added and no rule kind's Authority changes.
  • Generated artifacts (§4). None expected. If a new NotAcquired variant is added, schema/policy-input.schema.json regenerates through mise run schema and derived-check gates it.
  • Output & exit (§5). Pointer-only: the path and a cause token, never a byte of the unparseable file — whose contents are exactly what a parse failure would tempt an implementer to echo.
  • Commit / bump (§6). fix(rules) — patch until 0.1.0.
  • Test obligation (§7). Shown able to fail per CLOUD-418: the reproduction above is the case, and it is red on the tree as it stands. tests/privileged-lane.bats is where it belongs — that suite exists as of CLOUD-931 and deliberately ships WITHOUT this case rather than shipping it red. Assert the cause is distinguishable from Absent, so a policy cannot mistake "could not parse" for "not there" (CLOUD-874's second half, which this shares).
  • Blockers (§8). None. relatedTo CLOUD-874 (the collision cause reaching a nearby symptom, and the row this one supplies a fixture for), CLOUD-845 (the false-green class, and the row that introduced missing), CLOUD-931 (which added the second test tier that found this), CLOUD-849 (the three acquisition sites and their error mappings).

Acceptance

  • An unparseable declared source makes its module's missing clause fire, demonstrated in both directions.
  • The reported cause distinguishes "could not parse" from "absent".
  • The case lands in tests/privileged-lane.bats, driving the compiled binary rather than policy test.
  • documents_acquired is unchanged for the same tree — a second projection is not a second read.

Found by CLOUD-931's second test tier, on its first run: the suite was written to assert the module's own documented contract and the engine did not honour it.


Re-measured 2026-08-28 on 9b61f82 (CLOUD-910 wave 1, gate 1) — still live, and wider than this row says

This row is Done, and the defect is not fixed. It went Backlog → Done in one transition on 2026-08-25 with no In Progress → In Review for the fix itself, so the close is a batch artefact rather than a landed change. Reopened on that evidence.

The re-measurement also widens the subject. A throwaway repo, one row, and a probe module whose only clause raises on ANY entry in input.tree.missing:

violation contains {"rule": "probe", "verdict": "V-PROBE", "subjects": [{"path": path}]} if {
	some path in input.tree.missing
}

Three inputs, three runs, all batten check -Jexit 0, findings: [], no diagnostic:

input row column result
a path the tree does not have documents exit 0, no finding
a path the tree does not have sources exit 0, no finding
a path that EXISTS and does not parse documents exit 0, no finding

So input.tree.missing is not merely unfilled for a parse failure — it is unfilled on the tree surface entirely, and "absent" and "could not parse" are both indistinguishable from "clean". The third row is this issue's original subject; the first two are new here.

What that costs the retirement campaign, concretely

.claude/rules/policy-modules.md requires every module to write the missing clause. On the current engine every one of those clauses is dead, so each migrated gate ships a could-not-look channel that reports the same green as a clean tree — the exact failure missing was introduced to close. Three live modules already carry such a clause (privileged-lane, command-task-defined, and now mise-pin-agreement), and CLOUD-910's wave adds one per gate.

Measured cost on gate 1 alone: two bats cases that the bash answered with a loud exit 2 — a missing mise.toml and an unparseable .mcp.json — have no reachable successor, so both are declared changed in crates/batten/tests/mise_pin_agreement.rs rather than carried. The module keeps its V-PIN-AUTHORITY-UNREADABLE clause because the clause is right and the engine is what has to catch up; what it cannot keep is a compiled-binary case asserting it, for the reason crates/batten/tests/privileged_lane.rs already records — shipping one red bakes nothing in, and shipping one asserting the current behaviour bakes the defect in as the contract.

The declared #MUTANT row over that clause was removed for the same reason: a mutation over a predicate that cannot fire is a survivor by construction, and mutant would be right to report it.

What §2's acceptance should now read

The existing acceptance covers only the parse-failure input. It needs the absence input too, on both columns, and the cause token has to separate three states rather than two: acquired, absent, unparseable. crates/batten/tests/mise_pin_agreement.rs is where gate 1's two cases belong once the channel is filled, alongside the privileged-lane case §7 already names.

CLOUD-1268 Port-without-retirement: the ledger has no spelling for a suite whose subject survives — ~82s, 15 suites, and `session-start` is NOT one of them (its 135.9s is `mise install`, not the harness)

⚠️ CORRECTION 2026-08-31, AFTER THIS ROW WENT IN PROGRESS — session-start IS NOT A MEMBER, AND THE HEADLINE WAS WRONG BY 136 SECONDS

If you are working this row, read this before touching tests/session-start.bats. The
row was filed claiming "session-start, helpers and pre-commit-staging — 195.3s, 90% of the*
**class"*. The session-start third of that is false, and it was asserted from the class
it sat in rather than from the file — CLOUD-1166 arm 4, in the row that
quotes CLOUD-1166.

Read, not divided. tests/session-start.bats is 10 cases. Its setup() at :55-60
installs a mise stub that intercepts container-preflight, doctor and install:

if [ "$1" = install ] && [ -z "${SESSION_START_REAL_INSTALL:-}" ]; then exit 0; fi

Only two cases opt back in, through real_install_or_skip at :150 and :175. Eight run
against the stub and are cheap. So 135.9s is not 10 × 13.6s of provisioning — that figure was
arithmetic on the corpus total, and the per-case cost is nothing like uniform.

Three consequences, and the first kills its membership

  1. **A port to **crates/batten/tests/*.rs recovers almost nothing. The seconds are in
    mise install, not in the bats harness. A Rust case that runs the same install pays the
    same wall clock. Porting session-start moves 136 seconds from one lane to another.

  2. Its real remedy is one line, and it is not a retirement. The suite states it itself at
    :73: "Idempotent and warm, so the second install costs milliseconds." Warm
    mise install once in [tasks."test:bats"]. mise.toml is ungoverned
    governed_at_head selects mise-tasks/ paths and .bats suites and nothing else — so
    there is no V-SHELL-RULE-EDITED, no ledger arm, and no retirement. That is
    CLOUD-1198's shape exactly: build once, feed the seam the programs already
    declare.

  3. The check that settles it before any code, and it costs two minutes:

    mise install                                     # warm the toolchain
    mise run test:bats -- tests/session-start.bats    # time it warm vs 135.9s
    

    If it collapses, this is a one-line mise.toml change against the corpus's second-largest
    suite and it belongs to no bundle at all.

What that does to this row's numbers

filed corrected
members 16 15
class total 217.9s ~82.0s
"90% in three suites" 195.3s 59.4s in twohelpers 36.1 + pre-commit-staging 23.3

The remaining 13 members total ~22.6s, so this row is now a mechanism row with a modest**
**yield, not a 218-second recovery. That does not make it not worth doing — the ledger
spelling is the deliverable CLOUD-1156 deferred and nothing else can land
the class — but it must be scheduled against ~82s, and the makespan-floor argument below is
withdrawn: session-start leaves this row still standing at 135.9s, so this row does not
move the floor after Bundle A. Only its own remedy does.

And .claude/hooks/session-start.sh was never governed either

The premise that put it in this class — "the subject cannot die" — is true but irrelevant
for it. governed_at_head selects mise-tasks/ paths carrying a shebang or #MISE description, plus any .bats under tests/. A .claude/hooks/ program is in neither set,
so it is ungoverned for edits and for deletion. Filing it under "immortal subject" sent
every pass at it toward a retirement it never needed.

**Action: drop **session-start from §1's scope and from Acceptance, and file its
[tasks."test:bats"] warm-install remedy as its own row.


Why

SubjectFacts::died is .all() — every declared subject must be absent before a suite's
deletion is admitted. 16 suites declare a # subject: that is not a mise-tasks/ program:
mise.toml, hk.pkl, batten.toml, clippy.toml, install.sh, a .claude/hooks/ program,
or Rust source. Those subjects are not what the campaign retires, so .all() can never hold,
so the suites are undeletable by construction.

Measured at origin/main against bench/suites/RESULTS.md (138 suites, 1097.1s):

suite s # subject:
session-start 135.9 .claude/hooks/session-start.sh
helpers 36.1 tests/helpers.bash
pre-commit-staging 23.3 hk.pkl
lint-deno 5.6 mise.toml
hk-selection 5.0 hk.pkl
spawn-census 2.9 clippy.toml
lint-rego 2.6 mise.toml
install 2.2 install.sh
verify 1.5 mise.toml
fact-record-keying 1.0 crates/batten/src/facts.rs
commit-attribution 0.6 hk.pkl mise.toml
commit-convention 0.4 batten.toml mise.toml
task-fail-closed 0.4 mise.toml
git-hook 0.3 .claude/hooks/git-hook.sh
cross-check 0.1 mise.toml
zizmor-split 0.0 mise.toml
217.9 19.9%

Three suites are 195.3s — 90% of the class.

The row that owns this is Done, and it deferred the recovery to a row nobody filed

CLOUD-1156 closed 2026-08-31 04:36:52, inside the same ~4-second bulk cluster CLOUD-1253
documents. PR #777 landed policy/suite-subject-retirable.rego — the detector, which
refuses a new instance arriving unnoticed. That is a real deliverable and it is not this row's
complaint.

What it deferred is stated in the module's own header, policy/suite-subject-retirable.rego:47-51:

THE THIRD ROUTE IS STILL OPEN AND IS NOT THIS MODULE'S. A suite over batten.toml has
cases worth porting into crates/batten/tests/*.rs even though nothing dies — a port
WITHOUT a retirement, which the ledger has no spelling for. Each such port is its own row.
This module only refuses a NINETEENTH instance arriving unnoticed, which is the acceptance
CLOUD-1156 owes.

No such row exists. CLOUD-1156's own Acceptance says "All 19 suites are classified:
exempted, re-subjected, or ported-without-retirement" — they are exempted, which is one of
the three, and the 217.9s is untouched. This is CLOUD-1253's class stated precisely: the row**
**closed when its analysis was finished, not when its condition was gone.

Why the two obvious routes are refused, and why the third is landable

The module's header already establishes the first two, and they are not re-litigated here:

  • RE-SUBJECT — rewriting a # subject: line is an edit to a governed tests/**/*.bats.
    governed_at_head selects every bats suite, so it is V-SHELL-RULE-EDITED: one route, no
    bypass_env. The one admitted edit needs every removed line to name a path the same delta
    deletes, and a re-subjected header names paths that are staying. Refused.
  • AN IN-FILE MARKER — an added line that is neither a truncation nor a repointing. Refused
    by the same arm.

The third route is landable, and that is the finding. A ledger spelling for
port-without-retirement lives in policy/shell-retirement.rego and crates/batten/src/rules.rs
neither is governed by shell-retirement (governed_at_head selects mise-tasks/ paths
and .bats suites and nothing else). So unlike mutant.sh (CLOUD-1267) this class is not
trapped behind an unlandable edit. Nobody has tried.

And the ledger genuinely has no spelling today — checked rather than inherited.
shell-retirement.rego:62 declares
#MUTANT shell-subject-alive-unchecked|…|CLOUD-1130 — a row naming a subject this delta does NOT retire is refused, whichever of the four markers it carries, and named_and_alive
(:683-688) raises on exactly that. So CLOUD-1130 tightened this deliberately, and the new
spelling must be additive rather than a loosening of that arm — a suite deletion whose ledger
carries a complete port arm for every case, with the surviving subject named and its ongoing
coverage asserted elsewhere. .claude/rules/toolchain.md's "the ledger must name it too"
states the obligation; what is missing is a marker under which naming it is admitted.

It moves the makespan floor a second time, which is the reason to do it after Bundle A

test:bats runs --no-parallelize-within-files, so the lane's makespan cannot fall below the
longest single suite.

longest suite corpus
today land-lock 195.1 1097.1
after Bundle A (CLOUD-1170) session-start 135.9 866.2
after this row land 96.2 (CLOUD-1148's) ~648

Bundle A hands the floor to this class. It is the only other bundle that moves the floor
rather than the total, and it becomes the binding constraint the moment Bundle A lands.

Co-dispatchable remainder — 43.7s, and it shares NO mechanism with the above

Named here so it stops being unscheduled, not because it belongs to the same unit. Each is an
independent delta and any of them can be dropped without touching the spine:

what s state
CLOUD-1163 units 6, 7, 8, 10 — token-bench(+check), suite-bench-check(+suite-bench), gh-guard(+check), perf-gate(+perf-compare) 19.1 groomed 2026-08-31 as landing today; dispatched nowhere
hook-latency-drift 24.6 a member of CLOUD-1170's own table, "costed separately"; excluded from Bundle A by the dispatching session and in no bundle since

Units 6, 7, 8 each declare two subjects in one suite (tests/token-bench.bats:2,
tests/suite-bench-check.bats:2, tests/gh-guard.bats:2), so both members of each pair must
die in one delta — SubjectFacts::died is .all().


Refinement — Ready (give the ledger a port-without-retirement spelling, then spend it)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • **Authority boundary (§1). **policy/shell-retirement.rego and crates/batten/src/rules.rs
    for the spelling; then the suites themselves are deleted, one conserves arm per path.
    **No **mise-tasks/ **program and no **tests/**/*.bats is edited or added — every change to a
    suite here is a deletion, which is the only shape available.
  • Computable predicate (§2). A .bats deletion whose declared subject survives is admitted
    iff the ledger carries a complete port arm naming where every case went, and refused
    otherwise. named_and_alive's existing refusal stands for every other marker; this adds one
    admitted shape rather than weakening an arm.
  • The discriminating pair (§2), and it is the whole row. A deletion with a complete port
    arm passes; the same deletion with one case unported still refuses. Without the second,
    the spelling is a hole and CLOUD-908's finding — "retires_with conserves files, not logic…*
    *six cases have no successor anything in the tree can name" — recurs by design.
  • Coverage must not fall (§2). The subject is still alive and still needs testing. Each
    ported case names its crates/batten/tests/*.rs home, and the surviving subject's coverage
    is asserted there. A port that deletes 36 seconds of helpers.bats and lands nothing is the
    defect, not the deliverable.
  • Deliberately not in scope (§2). Changing SubjectFacts::died to any — that readmits
    the partial-retirement defect its own comment names, and CLOUD-1156 already rejected it.
    Re-subjecting any suite. Retiring release-tracking-check.sh or board-payloads.sh, the two
    CLOUD-1156 records as stranded by a governed co-subject — each needs this spelling first and
    then its own row.
  • **Weakens: rule-predicate-changed at rule[bats-tests-not-deleted].conserves
    Adding a fifth [rule.conserves] arm changes that rule's predicate, which config-lint raises as
    a smell judged against origin/main. This clause was written on 2026-09-01, AFTER the work

    **started, and is recorded that way deliberately. §1 mandated the [rule.conserves] change from
    the beginning and never groomed the admission that makes one landable, so this is a repair to the
    refinement rather than a groomed decision — and the claim receipt, minted before it, carries no
    weakens line. What actually admitted the smell on PR feat(policy): a fifth conserves arm for a port without a retirement #794 was the commit trailer ALONE, which is
    CLOUD-841's open hole (a receipt naming no weakening reads as "no receipt") rather than
    this clause working. Stated here so the next reader of this row does not mistake the sequence.
  • **Effect (§3). **read for the policy half.
  • Output and exit (§5). Pointer-only: the suite path and the unported case's name. Never a
    case body.
  • **Commit / bump (§6). **feat(policy) for the spelling, refactor(ci)! per port — patch (0.0.x, so a feat is a patch bump and a ! on a test-lane refactor carries no public surface with it).
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/; suite_subjects.rs
    is the existing tier. Shown able to fail per CLOUD-418, plus the anti-vacuity mirror: a
    conforming whole-file retirement still passes and an unexempted immortal subject is still
    refused, so this does not weaken suite-subject-retirable.
  • Blockers (§8). None. Sequenced **after **CLOUD-1170's Bundle A, since that is what makes
    session-start the floor and this row the binding constraint. relatedTo CLOUD-1156 (Done,
    detector landed, this is the recovery it deferred), CLOUD-1130 (whose arm this must extend
    rather than loosen), CLOUD-908 (the conserve-logic-not-files bar), CLOUD-1253 (the
    closed-while-the-condition-persists class this is an instance of), CLOUD-1163 and CLOUD-1170
    (the co-dispatchable remainder), CLOUD-1140 (suite cost).

Acceptance

  • The ledger admits a port-without-retirement, and refuses the same deletion with one case
    unported.
  • session-start, helpers and pre-commit-staging are ported and deleted — 59.4s,
    with every case's new home named and the surviving subject's coverage asserted.
    session-start is struck: its 135.9s is mise install in two cases, not the harness,
    so a port pays the same seconds. See the correction at the top of this row.
  • bench/suites/RESULTS.md regenerates with helpers and pre-commit-staging absent and the
    serial total down by the measured amount. ~~If ~~~~session-start~~ ~~still heads that table after~~~~
  and will still head the table; the makespan floor is its own remedy's to move.
* The remaining 13 members each carry a disposition: ported, or exempted with a reason that is
  not "the subject cannot die" — that one is now answered.
* `suite-subject-retirable` still refuses an unexempted new instance, asserted.

```Bundle
  and will still head the table; the makespan floor is its own remedy's to move.
* The remaining 13 members each carry a disposition: ported, or exempted with a reason that is
  not "the subject cannot die" — that one is now answered.
* `suite-subject-retirable` still refuses an unexempted new instance, asserted.

Found by subtracting four dispatched bundles from the corpus and asking what was left: this
class was in none of them, and the session that built those bundles had recorded
`session-start.bats` as *"the largest number in the tree with no owning row"* — which was
wrong twice over, since s owns it and had already closed.
```
</p>
</details>
<details>
<summary><a href="https://linear.app/buttoninc/issue/CLOUD-1061/three-declared-mutations-never-run-from-two-different-causes-mutant">CLOUD-1061 Three declared mutations never run, from two different causes — `mutant` reports `names-no-case` for a filter that matches, and the runner is in neither `verify` nor the gate</a></summary>
<p>

**Why**

`mise run mutant` reports **3 of 260 declared mutations not caught**, and none of
the three has ever discriminated anything:

```
board-payloads/board-payloads-decode-failure-is-an-empty-harvest names-no-case
attestation-check/provenance-absent-passes                       names-no-case
graph-check/receipt-carries-no-ids                               names-no-case
```

**They share a verdict and not a cause, and that is the finding.** Measured
2026-08-25 by running each declaration's field 3 as a `bats --filter` against its
own suite in the real tree:

| gate | filter matches in the real tree | `mutant` says |
| -- | -- | -- |
| `graph-check` | **0 cases** | `names-no-case` |
| `attestation-check` | 1 case | `names-no-case` |
| `board-payloads` | 1 case | `names-no-case` |

So `graph-check`'s is a **declaration** defect — field 3 names no case, which is
exactly [CLOUD-1034](https://linear.app/buttoninc/issue/CLOUD-1034/cloud-941-recurs-on-two-more-gates-ntia-checks-mutation-cannot-be)'s shape recurring a third time. The other two are a **runner**
defect: the filter is correct and resolves in the tree, and only fails inside
`mutant`'s copied working tree. One verdict is reporting two different faults, so
the reader is pointed at their declaration in the two cases where the declaration
is fine.

## Why it went unobserved

`mutant` is in **neither** `verify` nor the hk gate. `verify:gated` depends on
`tree-clean, ci, ci:quick, cross-check, darwin-link, zizmor, semver`; the gate
runs `mutant-census`, which asks only whether a gate is *declared or exempt* —
never whether its declarations *fire*. So a declaration can rot indefinitely and
the census keeps reporting full coverage, which is [CLOUD-418](https://linear.app/buttoninc/issue/CLOUD-418/a-new-gate-is-never-shown-to-fail-so-a-test-that-cannot-discriminate)'s defect one level
up: the thing that proves a suite discriminates is itself unobserved.

`board-payloads`' row landed in #688 earlier the same day and was never once
executed.

## Not the adjacent rows

* **Not** [CLOUD-1034](https://linear.app/buttoninc/issue/CLOUD-1034/cloud-941-recurs-on-two-more-gates-ntia-checks-mutation-cannot-be) — that row fixed two instances and is Done, so a comment
  there is stranded ([CLOUD-475](https://linear.app/buttoninc/issue/CLOUD-475/a-finding-against-landed-code-is-stranded-by-design-finding-sink-check)). This is a third instance **plus** a second cause
  that row never saw.
* **Not** [CLOUD-989](https://linear.app/buttoninc/issue/CLOUD-989/a-mutation-masked-by-a-downstream-guard-reports-as-survived-so-mutant) (a mutation masked by a downstream guard reports `SURVIVED`).
  Same family — the runner blaming the suite for a declaration defect — but that
  is a *surviving* mutation and these never ran at all.
* **Not** [CLOUD-944](https://linear.app/buttoninc/issue/CLOUD-944/mutant-run-concurrently-with-another-suite-reports-false-survivors-and) (concurrent runs report false survivors and false
  `case-already-red`). Adjacent and possibly upstream for the runner half, but
  the verdict differs: these report `names-no-case`, and the run that produced
  them was not contended. Worth checking together; not assumed here.

---

**Refinement — Ready**

*Refinement gate: [Definition of Ready & Done](<https://linear.app/buttoninc/document/definition-of-ready-and-done-e4e8defb6774>). This body carries only specializations.*

* **Source of truth (§1).** `mise-tasks/mutant.sh`'s suite resolution and its
  copied working tree, plus the three `#MUTANT` rows in the gates named above.
  No second list of declarations is introduced.
* **Computable predicate (§2).** Two, and they must stay distinct: a filter that
  matches no case in its own suite is a **declaration** fault and keeps
  `names-no-case`; a filter that matches in the tree and not in the copy is a
  **runner** fault and gets its own verdict naming the copy. Both are decidable
  by running the same filter twice, in the tree and in `$work`, and comparing —
  no judgement, no clock, no network.
* **Effect (§3).** `read` for the diagnosis. The runner already writes its copy.
* **Output & exit (§5).** Pointer-only and unchanged in shape: gate, slug,
  verdict, filter. The two verdicts differ by name so the reader is pointed at
  the right artifact.
* **Commit / bump (§6).** `fix(mutant)` → **patch** (collapsed, below 0.1.0).
  `mise-tasks/` is outside `crates/`, so release-plz cuts no release for this
  alone; the type's implied bump is what §6 declares.
* **Test obligation (§7).** `tests/mutant.bats`. **Shown able to fail**
  ([CLOUD-418](https://linear.app/buttoninc/issue/CLOUD-418/a-new-gate-is-never-shown-to-fail-so-a-test-that-cannot-discriminate)): a fixture gate whose field 3 names no case reports the declaration
  verdict; a fixture whose filter matches in the tree but whose suite is absent
  from the copy reports the runner verdict; and a correct declaration reports
  neither, so a change that emitted one verdict for everything fails.
* **Blockers (§8).** None.

## The declaration third is diagnosed to the character — take it first

`graph-check`'s is fully solved and needs no investigation, only the edit.
`mise-tasks/graph-check.sh:142` declares:

```
#MUTANT receipt-carries-no-ids|s@^	receipt_ids=.*@	receipt_ids=""@|a coherent board records which ids it judged
```

Field 3 reads **“records which ids it judged”**. The case in
`tests/graph-check.bats:949` is titled **“a coherent board records one receipt per****
****id it judged”**. The field is a paraphrase of the case rather than a substring of
it, so the `bats --filter` matches nothing — [CLOUD-1034](https://linear.app/buttoninc/issue/CLOUD-1034/cloud-941-recurs-on-two-more-gates-ntia-checks-mutation-cannot-be)'s exact shape, third
instance.

**The fix is field 3 only**, to a substring that resolves. Verified 2026-08-25
against the real suite:

```
$ tests/bats/bin/bats --filter 'a coherent board records one receipt per id' tests/graph-check.bats
1..1
ok 1 a coherent board records one receipt per id it judged
```

The sed script and slug are correct and must not be touched. After the edit,
confirm the mutation is actually CAUGHT rather than merely named — a filter that
resolves to a case the mutation does not redden is `SURVIVED`, which is a
different defect ([CLOUD-989](https://linear.app/buttoninc/issue/CLOUD-989/a-mutation-masked-by-a-downstream-guard-reports-as-survived-so-mutant)) and would need the conjunct re-examined.

**Why it is still sitting here.** `claim-check` refused this row with
`refined-this-session`: it was filed and refined in the same session that found
the defect, and [CLOUD-431](https://linear.app/buttoninc/issue/CLOUD-431/claim-check-lets-an-agent-certify-a-ready-block-it-wrote-seconds)'s question is whether a story was refined *before* the
session implementing it. That refusal is correct and was not bypassed, which is
why the fix is written out above instead of applied — any later session can take
this row cleanly and close this third in one edit.

**Acceptance**

* Each of the three is resolved to its own cause, and `graph-check`'s row names a
  case that exists.
* A declaration fault and a runner fault report different verdicts.
* `mise run mutant` reaches 260 of 260, or every remainder is a filed exemption.
* Whether `mutant` should be reachable from `verify` or the gate is **decided**
  rather than left implicit — today nothing runs it, which is why this was found
  by hand.

**What this row must NOT claim**

* Not that the runner half is understood. The filter demonstrably resolves in the
  tree and not in the copy; *why* is unestablished and is this row's first task.
* Not that adding `mutant` to `verify` is free. It ran ~260 mutations over the
  whole corpus in this measurement, and the cost of that on every lap is a
  separate decision from fixing the three.

---

*Found while landing* [CLOUD-901](https://linear.app/buttoninc/issue/CLOUD-901/alive-reports-a-running-task-as-crashed-so-the-one-command-for-is-it), which added `alive` *to the roster;* `alive`'s own
*three mutations were caught. The three above were pre-existing.*
</p>
</details>
<details>
<summary><a href="https://linear.app/buttoninc/issue/CLOUD-825/the-board-gates-already-compose-released-is-fed-devnull-and-three">CLOUD-825 The board gates already compose — `released` is fed `/dev/null` and three gates have no invoker, so four of the seven never decide</a></summary>
<p>

**Why**

Board dissonance — the board saying one thing while the tree says another — is not accumulating because gates are missing. Seven exist, every one a pure function of `get_issue` payloads on stdin:

| Gate | The dissonance it names |
| -- | -- |
| `graph-check` | graph incoherence, a dishonest column, Todo⇒Ready |
| `in-progress-drain` | In Progress that has landed; abandoned claims |
| `landed-check` | In Progress while its commits are on `main` |
| `done-pr-check` | Done while one of its own PRs is still open |
| `spec-ref-check` | a `CLOUD-N §M` citation naming a clause the issue lacks |
| `released` | In Review that a tag already shipped |
| `ready-lint` | the Ready block's checkable clauses |

## Corrected 2026-08-21, before this row was pulled

**This issue was filed claiming "zero of seven" and "nothing composes them". Both are false, and the corrected finding is narrower and much cheaper to fix.** The error came from grepping for `mise run <gate>` — which misses a call by path and misses a variable assignment. `graph-check:87` reads `lint="$(dirname "$0")/ready-lint"`, and no grep for the gate's name as a command finds it.

**They already compose. The chains exist; two of the three roots are never pulled and the third is fed nothing.**

| chain | state |
| -- | -- |
| `board-write-record` → `ready-lint` | **live** — a registered `PostToolUse` hook on `.*save_issue\|.*save_comment`, piping a projected payload at line 290, on every board write |
| `released` → `graph-check` → `ready-lint` | the root runs on every tag, **with** `</dev/null` |
| `in-progress-drain` → `landed-check` | the root has no invoker |
| `done-pr-check`, `spec-ref-check` | no invoker at all |

`claim-check:387` calls `ready-lint` by path too, so that gate has three callers, one of them a hook.

**The defect is one redirect and one missing caller.** `.github/workflows/release-plz.yml:187` runs:

```
mise run released "$tag" </dev/null
```

`released`'s own usage line distinguishes the arms — `mise run released v0.0.14` reports the refs a tag shipped, `mise run released v0.0.14 < payloads.json` restricts to In Review issues. The wired call takes the first, and that arm **returns before reaching the** `graph-check` **invocation at line 193**. So the composition that would sweep the board on every release exists, is wired, runs, and is handed nothing to decide over.

`hk.pkl` genuinely wires none of them — the three occurrences of these names in it are inside comments. That part stood.

**And the reason it is worth filing now is that everything else is already built.** Both halves of the fetch side landed on 2026-08-20:

* `board-payloads` ([CLOUD-782](https://linear.app/buttoninc/issue/CLOUD-782/a-board-sweep-pays-3x-the-tokens-its-gates-declare-because-get-issue)) recovers this session's own `get_issue` payloads from its transcript, byte-perfect, joined on `tool_use_id`, discriminating `get_issue` from `save_issue` by tool name — so a payload reaches a gate's stdin without an agent re-typing it, which is the forged-compliance shape [CLOUD-526](https://linear.app/buttoninc/issue/CLOUD-526/every-tracker-payload-gate-judges-text-the-agent-re-typed-not-the-row) measured seven times.
* `merged-pr-keys` ([CLOUD-814](https://linear.app/buttoninc/issue/CLOUD-814/landed-check-demands-merged-pr-evidence-and-nothing-in-the-repo)) emits `<CLOUD-id><TAB><pr-number>` for merged PRs — the evidence `landed-check --merged-prs` refuses to decide without, and which its own header records as having been produced by nothing but an ad-hoc `/tmp` script until that day.

Its header states the split: *"THIS DECIDES NOTHING, which is what lets it hold a credential… This is the FETCH side."* Both fetch sides now exist, both decider sides exist, and — per the correction above — **so does most of the joining**. What is missing is a payload set reaching `released`, and an invoker for the drain, `done-pr-check` and `spec-ref-check`.

**Why the In Progress column is the evidence, not the anecdote.** [CLOUD-469](https://linear.app/buttoninc/issue/CLOUD-469/in-progress-has-no-drain-landed-checks-predicate-is-applied-per-issue) measured that column 39 deep and was closed by building `in-progress-drain` — the *drain*, the thing that names what has gone stale. On 2026-08-21 the column is **28 deep** and `in-progress-drain` has never been asked about any of it. A live instance of what it would have caught, from the same day: one merge advanced [CLOUD-737](<https://linear.app/buttoninc/issue/CLOUD-737>), [CLOUD-738](https://linear.app/buttoninc/issue/CLOUD-738/gitrs-slice-2-move-ref-and-object-reads-in-process-deleting-the-end-of) and [CLOUD-739](https://linear.app/buttoninc/issue/CLOUD-739/gitrs-slice-3-derive-patch-identity-in-process-deleting-the-26-pinned) to In Review on mere mentions while none of their work was on `main`, and In Review is what `released` reads.

That is the shape of the whole defect, and it is non-negotiable rule 2 inverted: the gate is runnable and its *caller* is feedforward. `merged-pr-keys`'s header already names this inversion for its own case.

## Cost, and why one sweep is the affordable shape

[CLOUD-782](https://linear.app/buttoninc/issue/CLOUD-782/a-board-sweep-pays-3x-the-tokens-its-gates-declare-because-get-issue) measured the read: `list_issues` projects fields but its enum carries neither `attachments` nor `relations`, and `get_issue` returns both and takes no field selection, so a sweep pays ~4.5k tokens per issue to decide on ~1.6k. That ~3x is structural and this row does not touch it.

**One fetch, N gates is what makes it affordable** — the ~3x is paid once per sweep instead of once per gate, which is the difference between running seven gates and running one seven times. That is an argument for composing them, not a reason to defer.

---

**Refinement — Ready**

*Refinement gate: [Definition of Ready & Done](<https://linear.app/buttoninc/document/definition-of-ready-and-done-e4e8defb6774>). This body carries only specializations.*

* **Source of truth (§1).** Each gate stays the one authority on the dissonance it names, and — per the correction above — the composition mostly exists, so this row **must not build a second one**. A parallel composer beside `released` → `graph-check` → `ready-lint` would be the [CLOUD-351](https://linear.app/buttoninc/issue/CLOUD-351/final-asserts-three-of-its-four-needs-so-a-red-msrv-leaves-the-one) shape at the caller layer: two sweeps for one board, only the newer one consulted. `board-payloads` stays the one payload source and `merged-pr-keys` the one merged-PR evidence source.
* **What the work actually is, stated so it is not over-built.** Three things, smallest first: (a) feed `released` a payload set where `release-plz.yml:187` feeds it `/dev/null`, which reaches `graph-check` and `ready-lint` behind it; (b) give `in-progress-drain` an invoker, which reaches `landed-check` behind it; (c) give `done-pr-check` and `spec-ref-check` one. Whether (b) and (c) hang off the same entry point as (a) or a separate one is an implementation choice, not a second authority — what matters is that no gate's predicate is re-derived.
* **Computable predicate (§2).** Over one payload set covering Todo, In Progress, In Review and Done, every gate named above is reached — directly or through the chain that already calls it — and its exit code collected, and the verdict is the disjunction: exit `0` iff every gate exited `0`. `ready-lint` runs per Todo row rather than over the set, because it decides one Ready block at a time. A gate that cannot look — empty stdin, a projected-away field, a capped evidence file — is a **problem**, never a pass, following `merged-pr-keys`'s own reading that a truncated evidence file under-reports silently and `ci-local-parity`'s could-not-look-collapses-to-a-problem narrowing.
* **The report is a set, not a first failure.** A sweep that stopped at the first refusing gate would make the second dissonance invisible until the first was fixed, and the cost model above means the next look is not free. Every gate runs; the report names each refusal.
* **Effect (§3).** `read`. The sweep decides and reports; it moves no row. That asymmetry is the safety property: a sweeper authorised to write would be a board-editing robot deciding from a snapshot, and `board-payloads`'s own header records the failure — *"the corrected predicate returned the right* `relations` *beside a STALE* `status`*"* — measured twice on 2026-08-20. Recover structure from the cache; re-read a row before deciding its state.
* **Output & exit (§5).** Pointer-only per non-negotiable rule 4 — issue keys, gate names and counts, never a line of a body. Exit `0` clean / `1` dissonance named / `2` could not look, matching the three gates it composes so the whole thing stays one contract with no per-verb exception (house-style §6–§7).
* **Scheduling is a second decision and is deliberately not this row's.** What lands here is the composer, runnable by hand. Whether it also runs on a cron, at Stop, or as a `land` postlude is a trigger question with its own cost — and [CLOUD-812](https://linear.app/buttoninc/issue/CLOUD-812/cloud-180s-narrowing-stopped-at-ciyml-so-two-pr-workflows-still) measured 96 idle cron ticks/day flooring at ~2,900 billed min/month, so a schedule is not free and must not be added as an afterthought. Ship the caller; file the trigger.
* **Commit / bump (§6).** `feat` → **patch until** `0.1.0`; below that release-plz patches whatever the type says.
* **Test obligation (§7).** `tests/board-sweep.bats`, over fixture payload sets rather than a live board, in the shape `tests/ci-local-parity.bats` uses. Shown able to fail per [CLOUD-418](https://linear.app/buttoninc/issue/CLOUD-418/a-new-gate-is-never-shown-to-fail-so-a-test-that-cannot-discriminate): a fixture set containing an In Progress row whose commits are on `main` must exit non-zero and name `in-progress-drain`. **Plus the case this row's own correction demands** — a fixture set piped where `release-plz.yml` pipes `/dev/null` must reach `graph-check`, proven by a fixture whose only defect is one `graph-check` names, since that is the exact path the current wiring runs past. Plus: a set with no dissonance exits `0`; an **empty** stdin exits `2` rather than reporting clean, which is the anti-vacuity case and the one a composer gets wrong; a gate exiting `2` is not laundered into the `1` lane; and the report carries no body text.
* **Blockers (§8).** None. `relatedTo` [CLOUD-469](https://linear.app/buttoninc/issue/CLOUD-469/in-progress-has-no-drain-landed-checks-predicate-is-applied-per-issue), whose drain this completes; `relatedTo` [CLOUD-782](https://linear.app/buttoninc/issue/CLOUD-782/a-board-sweep-pays-3x-the-tokens-its-gates-declare-because-get-issue) and [CLOUD-814](https://linear.app/buttoninc/issue/CLOUD-814/landed-check-demands-merged-pr-evidence-and-nothing-in-the-repo), the two fetch halves it composes; `relatedTo` [CLOUD-804](https://linear.app/buttoninc/issue/CLOUD-804/landed-check-reads-a-citation-as-a-landing-a-bare-grep-over-mains), which moved half of landedness to the caller and is why a caller has to exist.

**Done**

`main` carries the sweep, a fixture set carrying a landed-but-In-Progress row is red where a clean set is green, and CI green on the merge commit landed by fast-forward.

## Not in scope

Changing any gate's predicate; the ~3x read cost, which is the tracker's field-projection gap and [CLOUD-782](https://linear.app/buttoninc/issue/CLOUD-782/a-board-sweep-pays-3x-the-tokens-its-gates-declare-because-get-issue)'s; and the schedule, per §5 above.
</p>
</details>
<details>
<summary><a href="https://linear.app/buttoninc/issue/CLOUD-1278/an-overrides-articulation-dies-with-the-container-so-the-forcing">CLOUD-1278 An override's articulation dies with the container, so the forcing function is a toll with no audit trail and no reader</a></summary>
<p>

**Why**

[CLOUD-1051](https://linear.app/buttoninc/issue/CLOUD-1051/replace-prose-only-and-filed-here-override-passwords-with-issued) replaced the override passwords with issued capabilities, and [CLOUD-1120](https://linear.app/buttoninc/issue/CLOUD-1120/a-spent-admission-does-not-suppress-its-finding-so-v-filed-over-own) made a spent admission actually suppress its finding. What neither did is give the articulation a reader.

`admission::store_dir` resolves under the OS data directory — in this repository a container the platform reclaims — so the reasoning an override cost is legible only inside the session that wrote it, and only until that session ends. There is no read verb either: `override` offers `request` and `spend` and nothing that can show a record back. Measured on this branch: after spending four real admissions, none of the four articulations could be recovered.

`lib.rs`'s own output site made it worse deliberately, under a comment reading *"POINTER, NEVER THE ANSWERS (rule 4) … the reasoning the author typed stays in the store where it was written"*, with a test asserting no answer text crosses stdout.

**That reading of rule 4 is wrong, and it is the defect.** Rule 4 stops a gate republishing REPOSITORY content — a secret it scanned, a file it read, a subject line somebody typed. An articulation is none of those: it is the caller's own words, composed for the express purpose of being read by a reviewer. `admission.rs`'s own header already says an address *"authorizes nothing on its own"* so a record is *"safe to print, log, quote in a commit and leave in a transcript"*, and `refusal.rs` calls this *"rule 4's deliberate inversion"*. Honouring the letter of a rule that was not about them cost the design its entire point.

The stated intent for the mechanism was: increase the token cost of editing protected paths without preventing it, while deterministically producing an audit trail that lets a later reader diagnose what prose led to a bad decision. Half of that shipped. The half that shipped is the toll.

**The destination**

A commit message. It is durable by construction, travels with the change it justifies, needs no network to read, and is already in front of every reviewer and review bot per-commit. A pull request body loses on all three counts that matter: it is mutable after the fact, it is one blob for N commits so the per-write binding is lost, and writing it is `land.sh`'s job — authored shell frozen by `V-SHELL-RULE-EDITED`.

**Refinement — Ready**

*Refinement gate: [Definition of Ready & Done](<https://linear.app/buttoninc/document/definition-of-ready-and-done-e4e8defb6774>). This body carries only specializations.*

* **Source of truth (§1). **`crates/batten/src/admission.rs` (the record and its address), `crates/batten/src/commit.rs` (the clause), `crates/batten/src/lib.rs` (the `spend` output site and the `commit check` wiring), `crates/batten/src/git.rs` (the range reader).
* **Computable predicate (§2). **`batten commit check <base>..<head>` exits 2 naming every protected path a commit in the range wrote with no verifying `Admits:` block, and exits 0 when each carries one. Two findings, not one: `admits` for absent, `admits-tampered` for a block that does not hash to the address it names. The same clause runs at `commit-msg` over the staged set, so the unarticulated commit is never created.
* **Verifiable with the store deleted (§2).** The block spells out every binding field, so `Articulation::recomputes` is a pure function of the message. A block carrying only the address would be a pointer into a store that has already been reclaimed, which is the failure being fixed. This is also what makes it a CI tier rather than a local one.
* **Effect (§3). **`commit check` stays read-effect. `override spend` gains output only — it already performs the store write this reads back.
* **Output & exit (§5).** The one contract: `0` clean, `2` a finding, `1` could-not-look. Findings are pointer lines — `<sha8> <field> <path>`. The path is a pointer, which §6 names outright as an allowed shape; without it an author is told a commit lacks an articulation and left to work out which of its protected paths.
* **Commit / bump (§6). **`feat(commit)`, patch — the crate is below 0.1.0, where a feature is a patch bump rather than a minor one.
* **Test obligation (§7). **`crates/batten/tests/commit_admission.rs` over the compiled binary, with `an_unarticulated_protected_write_is_refused` as the premise case — a fixture whose protected glob selected nothing yields no paths, so every admitting case would otherwise pass over a clause that never fired. `the_clause_needs_no_store_to_decide` removes the store and asserts the block still verifies.
* **Blockers (§8).** None. `relatedTo` [CLOUD-1051](https://linear.app/buttoninc/issue/CLOUD-1051/replace-prose-only-and-filed-here-override-passwords-with-issued), [CLOUD-1120](https://linear.app/buttoninc/issue/CLOUD-1120/a-spent-admission-does-not-suppress-its-finding-so-v-filed-over-own), [CLOUD-1125](https://linear.app/buttoninc/issue/CLOUD-1125/an-admission-binds-head-but-land-rebases-before-the-gate-that-reads-it), [CLOUD-1050](https://linear.app/buttoninc/issue/CLOUD-1050/cloud-122s-refusal-contract-has-no-mechanism-and-the-rego-migration-is).

**Acceptance**

* A protected write with no block is exit 2 and names the path; the same commit with a spent admission's block is exit 0.
* An edited block reports `admits-tampered`, never `admits` — rolling both into "missing" would let a doctored articulation read as an honest omission, which is the one failure a content-addressed record exists to expose.
* The clause decides identically with the override store deleted.
* `override spend` prints a block that `commit check` accepts, so the verb that issues and the gate that reads cannot disagree about the format.

**Not in this issue**

A `batten override show` read verb. `spend` prints the block at the one moment it is needed, so the remaining gap is recovering a block for an admission already spent — real, but a follow-on.

[CLOUD-1125](https://linear.app/buttoninc/issue/CLOUD-1125/an-admission-binds-head-but-land-rebases-before-the-gate-that-reads-it)'s head-binding problem is adjacent and NOT solved here: the block records `head` for the reader, and `recomputes` does not compare it against the commit's actual parent, so a rebase leaves the block verifying. That is what makes this survive the landing loop; it is not a fix for the gate [CLOUD-1125](https://linear.app/buttoninc/issue/CLOUD-1125/an-admission-binds-head-but-land-rebases-before-the-gate-that-reads-it) describes.

Refs: [CLOUD-1051](https://linear.app/buttoninc/issue/CLOUD-1051/replace-prose-only-and-filed-here-override-passwords-with-issued), [CLOUD-1120](https://linear.app/buttoninc/issue/CLOUD-1120/a-spent-admission-does-not-suppress-its-finding-so-v-filed-over-own), [CLOUD-1125](https://linear.app/buttoninc/issue/CLOUD-1125/an-admission-binds-head-but-land-rebases-before-the-gate-that-reads-it), [CLOUD-1050](https://linear.app/buttoninc/issue/CLOUD-1050/cloud-122s-refusal-contract-has-no-mechanism-and-the-rego-migration-is)
</p>
</details>
<!-- linear-review-link -->
<p><a href="https://linear.app/buttoninc/review/refactorci-bundle-g-retire-the-members-that-port-record-a-read-verdict-cd63de7958d3">Review in Linear</a></p>

@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 581d120a-c5c9-48cb-8bd0-780b491c35b1

📥 Commits

Reviewing files that changed from the base of the PR and between 5929fbf and 26ae231.

📒 Files selected for processing (1)
  • bench/suites/RESULTS.md
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 230afe25-2124-4aed-bed3-28ea5bd289c2

📥 Commits

Reviewing files that changed from the base of the PR and between 0de5709 and 5929fbf.

📒 Files selected for processing (17)
  • .claude/rules/policy-modules.md
  • .claude/rules/toolchain.md
  • batten.toml
  • crates/batten/tests/gh_guard.rs
  • crates/batten/tests/memories.rs
  • crates/batten/tests/rules_drift.rs
  • crates/batten/tests/staged_facts.rs
  • mise-tasks/gh-guard-check.sh
  • mise-tasks/gh-guard.sh
  • mise-tasks/memories-check.sh
  • mise-tasks/rules-drift.sh
  • mise.toml
  • policy/memories.rego
  • policy/rules-drift.rego
  • tests/gh-guard.bats
  • tests/memories-check.bats
  • tests/rules-drift.bats
💤 Files with no reviewable changes (7)
  • mise-tasks/gh-guard.sh
  • mise-tasks/rules-drift.sh
  • mise-tasks/memories-check.sh
  • tests/gh-guard.bats
  • mise-tasks/gh-guard-check.sh
  • tests/rules-drift.bats
  • tests/memories-check.bats

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


📝 Walkthrough

Walkthrough

Added compiled memory-graph and rules-drift policies with typed verdicts, routes, task wrappers, and integration tests. Removed the retired shell-based gh-guard, memories-check, and rules-drift tasks and their Bats suites. Added compiled hook acceptance tests and staged-input regression tests. Updated policy documentation for measured unreadable-source behavior and lifecycle bypass handling.

Merge Risk: ⚪ Minimal · up to 5929f

This change is merge-ready after normal checks and review; no actionable merge-blocking risk remains.


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@wenzowski wenzowski changed the title refactor(ci): Bundle G — retire the members that port, record a read verdict for the ones that do not CLOUD-843 bundle G: retire gh-guard, memories-check and rules-drift; four members refuted with measurements Sep 1, 2026
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch 2 times, most recently from fe1a24b to bb9e875 Compare September 1, 2026 05:43
wenzowski added a commit that referenced this pull request Sep 1, 2026
Two of Bundle G's seven members. The other five were read and did not land;
each verdict is recorded on the row that owns it rather than carried as a
category, per CLOUD-1164's own rule.

`gh-guard` + `gh-guard-check` + `tests/gh-guard.bats` (0.9s, 23 cases). The
campaign's one retirement whose successor was already green: CLOUD-312 made
`PreToolUse` a single entry, the `gh` lifecycle moved into `batten.toml`'s four
`shape` rows, and that file calls them "the ported `gh-guard`" at :274. Nothing
has invoked either program since — absent from `.claude/settings.json`, from
`mise.toml`'s task graph, from `hk.pkl` and from every workflow, their only
executions were their own suite and `mise run mutant`. All 23 cases were
replayed against the compiled binary and the committed table BEFORE anything was
deleted: 24 of 25 command shapes agree exactly, and the twenty-fifth is
`gh pr ready`, allowed by this predicate and refused by `ready-needs-receipts`
doing its own job. That case asserts the rule id rather than the aggregate
verdict, because reading the exit code would report one rule's correct arrival
as this rule's regression.

`memories-check` + `tests/memories-check.bats` (3.8s, 11 cases) onto
`policy/memories.rego`. Its own header already claimed to be "a pure function of
the tracked tree", and it held: `input.tree.tracked` answers which memories
exist, `input.tree.lines` answers who references one. The reference grammar the
shell restated internally — its comment said the syntax lived "HERE AND ONLY
HERE" — is now two `[[pattern]]` rows, so the one-spelling property is
structural rather than an instruction to the next author: an inline regex is
refused at load, and the module cannot re-spell either token even by accident.

THE PORT FOUND A DOC/MECHANISM DRIFT AND IT IS CORRECTED HERE RATHER THAN
CARRIED FORWARD. `.claude/rules/toolchain.md` claimed `BATTEN_GH_GUARD_BYPASS`
was "the `bypass_env` these rows DECLARE rather than the engine's global hatch".
It is not, and `batten.toml:273` says so in as many words: these four "WOULD
DECLARE `bypass_env = "BATTEN_GH_GUARD_BYPASS"` AND DO NOT YET", deferred to
CLOUD-1027 because adding the key reads to `config-lint` as
`rule-predicate-changed` and owes a groomed `Weakens:` clause first. The test was
written against the rules file, went red against the tree, and the tree was
right. Two cases now pin both directions.

That also settles what this change does NOT conserve. `gh-guard.sh:16` honoured
`BATTEN_GH_GUARD_BYPASS`, and nothing has invoked that program since CLOUD-312,
so the variable has been inert for that whole span. Deleting the file removes
dead code that still appeared to offer a hatch; it does not remove one.
`rules-drift` gates values `.claude/rules/*.md` restates against the mechanisms
that own them, and it did not catch this.

Four file arms and 34 case arms, `$MUTANT_GATES` down by two, and both modules
carry a `#MUTANT-EXEMPT` naming CLOUD-1267 — `mutant` resolves a gate's suite as
`tests/$gate.bats` and `V-SHELL-RULE-ADDED` refuses adding one, so a re-homed
mutation has no named case to turn red and the exemption is the honest record of
that rather than a gap.

ONE STALE REFERENCE IS DELIBERATELY LEFT. `.github/workflows/ci.yml:217` carries
a comment naming "the gh-guard program" as a runtime user of `jq`. The file is
`protected`, the refusal's own remedy is "change it in a pull request", and
spending the hatch on a comment is not worth the precedent. `jq` is still needed
by the remaining shell gates, so the pin is unaffected.

THE CORPUS IS REGENERATED IN THIS COMMIT rather than a following one, because
`suite-bench-check` runs in the pre-commit gate and is bidirectional set
equality against `bench/suites/RESULTS.md` -- a deletion and its corpus cannot
be separated without the intervening commit being red. Regenerated per the
file's own instruction (`mise run test:bats`, then `mise run suite-bench
--write`), never hand-edited.

136 suites, 968.7s serial on this container.

THE SAVING IS 4.7s AND NOT THE 128.4s THE TOTALS DIFFER BY. The previous corpus
recorded 1097.1s on a different run, and subtracting across runs is the error
`ab6efb9` corrected in its own message: the two numbers never shared a baseline.
The defensible figure is the two suites' own share of the corpus they were
measured in -- `tests/gh-guard.bats` 0.9s and `tests/memories-check.bats` 3.8s
of 1097.1s, so 0.43%. The rest of the delta is machine noise and is not claimed.

Refs: CLOUD-1163
Refs: CLOUD-1164

Admits: 9029ddfc6fd45a07a78a5a2eca89d5a78f64e6c9b7940ccd888ccdab07425262
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bb9e875
Admits-epoch: a5e71ccbe2c342c3d9c2ca4939939d9d69e693944afa7b19906f5f044daee2d6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The gh-guard, memories-check and rules-drift programs stay alive as a second implementation of predicates the engine already owns or is taking over, and CLOUD-843 cannot retire any gate at all -- every retirement in the campaign needs its rows here, so refusing this write refuses the campaign rather than this change.
Admits-answer-precondition: A retirement under CLOUD-843 moves a predicate from a shell program into a policy module, and a policy module is unreachable until batten.toml declares its [[rule]] row, its [[pattern]] rows and every [[verdict]] token it can raise -- the engine refuses at LOAD a module raising a token no row declares. No other surface can add those rows: they are the config that makes the module exist. The write is additive blocks appended to the file, visible in the diff of PR #796, 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 rule declaration; there is no other surface that owns it, and the rule this change adds cannot be declared anywhere else. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to be undone -- restoring the file would delete the rows the new module needs to load.
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-843 bundle G, member 6 (CLOUD-1150). `mise-tasks/rules-drift.sh` and
`tests/rules-drift.bats` are deleted; all four predicates land as
`policy/rules-drift.rego` with `crates/batten/tests/rules_drift.rs` as the
compiled-binary tier. The `$MUTANT_GATES` entry is dropped and `hk.pkl`'s
step keeps its name through a `[tasks.rules-drift]` wrapper, so no caller
moved.

Ledger: two deleted paths, thirty deleted `@test` cases. Twenty-five are
carried; six are `changed:` and say why.

Four anti-vacuity guards change shape together. The predecessor was a TASK
with a call site and refused at startup when an authority would not read; a
`[[rule]]` has no call site, so an unconditional guard would speak in every
fixture repository inheriting this config -- the scoping defect CLOUD-1164
records for `tree-clean`. Each is now conditioned on some prose actually
claiming against the authority.

Two engine limits shaped the module and are recorded rather than absorbed:

* regorus carries no `walk`, so predicate 3's recursive descent became one
  fixed path. Measured, each generated schema carries exactly one such node.
  That buys a vacuity hole a moved schema would fall into silently, so
  `schema_vacuous` covers the read-but-empty case.
* An unreadable declared source does not reach `input.tree.missing` -- it
  stops the WHOLE RULE evaluating, silencing predicates that never read it.
  So the row declares its JSON authorities under `sources` (the glob form),
  which keeps the rule alive across an absent file and lets the module raise
  its own could-not-look. Nothing recovers the unparseable case; the
  predecessor caught it and this does not.

Also here, both found by running the gates over this diff:

* `crates/batten/tests/staged_facts.rs` gains the measurement that keeps
  `lock-complete` out of this bundle. `Format::for_path` searches
  `Format::extensions()` and no variant owns `lock`, so a declared `.lock`
  is `UnknownFormat` before a byte is parsed even though its staged bytes
  are valid TOML -- and `rules.rs:7129` pushes the cause into `missing`,
  where it does not arrive. The loss is downstream of the push.
* `crates/batten/tests/gh_guard.rs` no longer names where this repository is
  hosted (`no-origin-literal-in-fixtures`), rewritten rather than exempted.

`.claude/rules/policy-modules.md` told authors CLOUD-1049 had shipped and the
could-not-look channel worked. It has not; the measured three-way table
replaces that claim, and CLOUD-1049 is reopened with the reproduction.

Refs: CLOUD-843, CLOUD-1150, CLOUD-1163, CLOUD-1164, CLOUD-1049

Admits: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bb9e875
Admits-epoch: a5e71ccbe2c342c3d9c2ca4939939d9d69e693944afa7b19906f5f044daee2d6
Admits-author: alec@wenzowski.com
Admits-prev: 9029ddfc6fd45a07a78a5a2eca89d5a78f64e6c9b7940ccd888ccdab07425262
Admits-answer-lost: mise-tasks/rules-drift.sh cannot retire, so CLOUD-1150 stays open and the shell layer CLOUD-843 exists to retire keeps this gate. The predicates would have to stay in bash, where shell-retirement refuses editing them and the second test tier that found this PR's engine defect does not exist.
Admits-answer-precondition: This commit adds policy/rules-drift.rego, and a module is unreachable until batten.toml declares its [[rule]] row, the five [[pattern]] rows its predicates read, and every [[verdict]] token it can raise -- the engine refuses at LOAD a module raising a token no row declares, and refuses an inline regex so the patterns cannot live in the module either. batten.toml is the only surface that owns those declarations. The write is additive blocks at the end of the file, in the diff of PR #796 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 [[rule]], [[pattern]] and [[verdict]] declaration -- there is nowhere else these can be written, and policy-modules.md refuses the inline alternative for the patterns specifically. R-RESTORE-IT does not apply because the write is the deliberate content of the change rather than damage: restoring the file would delete the rows the new module needs to load, leaving a module that loads and decides nothing.
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch from bb9e875 to cee63e4 Compare September 1, 2026 05:46
wenzowski added a commit that referenced this pull request Sep 1, 2026
Two of Bundle G's seven members. The other five were read and did not land;
each verdict is recorded on the row that owns it rather than carried as a
category, per CLOUD-1164's own rule.

`gh-guard` + `gh-guard-check` + `tests/gh-guard.bats` (0.9s, 23 cases). The
campaign's one retirement whose successor was already green: CLOUD-312 made
`PreToolUse` a single entry, the `gh` lifecycle moved into `batten.toml`'s four
`shape` rows, and that file calls them "the ported `gh-guard`" at :274. Nothing
has invoked either program since — absent from `.claude/settings.json`, from
`mise.toml`'s task graph, from `hk.pkl` and from every workflow, their only
executions were their own suite and `mise run mutant`. All 23 cases were
replayed against the compiled binary and the committed table BEFORE anything was
deleted: 24 of 25 command shapes agree exactly, and the twenty-fifth is
`gh pr ready`, allowed by this predicate and refused by `ready-needs-receipts`
doing its own job. That case asserts the rule id rather than the aggregate
verdict, because reading the exit code would report one rule's correct arrival
as this rule's regression.

`memories-check` + `tests/memories-check.bats` (3.8s, 11 cases) onto
`policy/memories.rego`. Its own header already claimed to be "a pure function of
the tracked tree", and it held: `input.tree.tracked` answers which memories
exist, `input.tree.lines` answers who references one. The reference grammar the
shell restated internally — its comment said the syntax lived "HERE AND ONLY
HERE" — is now two `[[pattern]]` rows, so the one-spelling property is
structural rather than an instruction to the next author: an inline regex is
refused at load, and the module cannot re-spell either token even by accident.

THE PORT FOUND A DOC/MECHANISM DRIFT AND IT IS CORRECTED HERE RATHER THAN
CARRIED FORWARD. `.claude/rules/toolchain.md` claimed `BATTEN_GH_GUARD_BYPASS`
was "the `bypass_env` these rows DECLARE rather than the engine's global hatch".
It is not, and `batten.toml:273` says so in as many words: these four "WOULD
DECLARE `bypass_env = "BATTEN_GH_GUARD_BYPASS"` AND DO NOT YET", deferred to
CLOUD-1027 because adding the key reads to `config-lint` as
`rule-predicate-changed` and owes a groomed `Weakens:` clause first. The test was
written against the rules file, went red against the tree, and the tree was
right. Two cases now pin both directions.

That also settles what this change does NOT conserve. `gh-guard.sh:16` honoured
`BATTEN_GH_GUARD_BYPASS`, and nothing has invoked that program since CLOUD-312,
so the variable has been inert for that whole span. Deleting the file removes
dead code that still appeared to offer a hatch; it does not remove one.
`rules-drift` gates values `.claude/rules/*.md` restates against the mechanisms
that own them, and it did not catch this.

Four file arms and 34 case arms, `$MUTANT_GATES` down by two, and both modules
carry a `#MUTANT-EXEMPT` naming CLOUD-1267 — `mutant` resolves a gate's suite as
`tests/$gate.bats` and `V-SHELL-RULE-ADDED` refuses adding one, so a re-homed
mutation has no named case to turn red and the exemption is the honest record of
that rather than a gap.

ONE STALE REFERENCE IS DELIBERATELY LEFT. `.github/workflows/ci.yml:217` carries
a comment naming "the gh-guard program" as a runtime user of `jq`. The file is
`protected`, the refusal's own remedy is "change it in a pull request", and
spending the hatch on a comment is not worth the precedent. `jq` is still needed
by the remaining shell gates, so the pin is unaffected.

THE CORPUS IS REGENERATED IN THIS COMMIT rather than a following one, because
`suite-bench-check` runs in the pre-commit gate and is bidirectional set
equality against `bench/suites/RESULTS.md` -- a deletion and its corpus cannot
be separated without the intervening commit being red. Regenerated per the
file's own instruction (`mise run test:bats`, then `mise run suite-bench
--write`), never hand-edited.

136 suites, 968.7s serial on this container.

THE SAVING IS 4.7s AND NOT THE 128.4s THE TOTALS DIFFER BY. The previous corpus
recorded 1097.1s on a different run, and subtracting across runs is the error
`ab6efb9` corrected in its own message: the two numbers never shared a baseline.
The defensible figure is the two suites' own share of the corpus they were
measured in -- `tests/gh-guard.bats` 0.9s and `tests/memories-check.bats` 3.8s
of 1097.1s, so 0.43%. The rest of the delta is machine noise and is not claimed.

Refs: CLOUD-1163
Refs: CLOUD-1164

Admits: 9029ddfc6fd45a07a78a5a2eca89d5a78f64e6c9b7940ccd888ccdab07425262
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bb9e875
Admits-epoch: a5e71ccbe2c342c3d9c2ca4939939d9d69e693944afa7b19906f5f044daee2d6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The gh-guard, memories-check and rules-drift programs stay alive as a second implementation of predicates the engine already owns or is taking over, and CLOUD-843 cannot retire any gate at all -- every retirement in the campaign needs its rows here, so refusing this write refuses the campaign rather than this change.
Admits-answer-precondition: A retirement under CLOUD-843 moves a predicate from a shell program into a policy module, and a policy module is unreachable until batten.toml declares its [[rule]] row, its [[pattern]] rows and every [[verdict]] token it can raise -- the engine refuses at LOAD a module raising a token no row declares. No other surface can add those rows: they are the config that makes the module exist. The write is additive blocks appended to the file, visible in the diff of PR #796, 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 rule declaration; there is no other surface that owns it, and the rule this change adds cannot be declared anywhere else. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to be undone -- restoring the file would delete the rows the new module needs to load.
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-843 bundle G, member 6 (CLOUD-1150). `mise-tasks/rules-drift.sh` and
`tests/rules-drift.bats` are deleted; all four predicates land as
`policy/rules-drift.rego` with `crates/batten/tests/rules_drift.rs` as the
compiled-binary tier. The `$MUTANT_GATES` entry is dropped and `hk.pkl`'s
step keeps its name through a `[tasks.rules-drift]` wrapper, so no caller
moved.

Ledger: two deleted paths, thirty deleted `@test` cases. Twenty-five are
carried; six are `changed:` and say why.

Four anti-vacuity guards change shape together. The predecessor was a TASK
with a call site and refused at startup when an authority would not read; a
`[[rule]]` has no call site, so an unconditional guard would speak in every
fixture repository inheriting this config -- the scoping defect CLOUD-1164
records for `tree-clean`. Each is now conditioned on some prose actually
claiming against the authority.

Two engine limits shaped the module and are recorded rather than absorbed:

* regorus carries no `walk`, so predicate 3's recursive descent became one
  fixed path. Measured, each generated schema carries exactly one such node.
  That buys a vacuity hole a moved schema would fall into silently, so
  `schema_vacuous` covers the read-but-empty case.
* An unreadable declared source does not reach `input.tree.missing` -- it
  stops the WHOLE RULE evaluating, silencing predicates that never read it.
  So the row declares its JSON authorities under `sources` (the glob form),
  which keeps the rule alive across an absent file and lets the module raise
  its own could-not-look. Nothing recovers the unparseable case; the
  predecessor caught it and this does not.

Also here, both found by running the gates over this diff:

* `crates/batten/tests/staged_facts.rs` gains the measurement that keeps
  `lock-complete` out of this bundle. `Format::for_path` searches
  `Format::extensions()` and no variant owns `lock`, so a declared `.lock`
  is `UnknownFormat` before a byte is parsed even though its staged bytes
  are valid TOML -- and `rules.rs:7129` pushes the cause into `missing`,
  where it does not arrive. The loss is downstream of the push.
* `crates/batten/tests/gh_guard.rs` no longer names where this repository is
  hosted (`no-origin-literal-in-fixtures`), rewritten rather than exempted.

`.claude/rules/policy-modules.md` told authors CLOUD-1049 had shipped and the
could-not-look channel worked. It has not; the measured three-way table
replaces that claim, and CLOUD-1049 is reopened with the reproduction.

Refs: CLOUD-843, CLOUD-1150, CLOUD-1163, CLOUD-1164, CLOUD-1049

Admits: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bb9e875
Admits-epoch: a5e71ccbe2c342c3d9c2ca4939939d9d69e693944afa7b19906f5f044daee2d6
Admits-author: alec@wenzowski.com
Admits-prev: 9029ddfc6fd45a07a78a5a2eca89d5a78f64e6c9b7940ccd888ccdab07425262
Admits-answer-lost: mise-tasks/rules-drift.sh cannot retire, so CLOUD-1150 stays open and the shell layer CLOUD-843 exists to retire keeps this gate. The predicates would have to stay in bash, where shell-retirement refuses editing them and the second test tier that found this PR's engine defect does not exist.
Admits-answer-precondition: This commit adds policy/rules-drift.rego, and a module is unreachable until batten.toml declares its [[rule]] row, the five [[pattern]] rows its predicates read, and every [[verdict]] token it can raise -- the engine refuses at LOAD a module raising a token no row declares, and refuses an inline regex so the patterns cannot live in the module either. batten.toml is the only surface that owns those declarations. The write is additive blocks at the end of the file, in the diff of PR #796 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 [[rule]], [[pattern]] and [[verdict]] declaration -- there is nowhere else these can be written, and policy-modules.md refuses the inline alternative for the patterns specifically. R-RESTORE-IT does not apply because the write is the deliberate content of the change rather than damage: restoring the file would delete the rows the new module needs to load, leaving a module that loads and decides nothing.
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch from cee63e4 to dfe5d9d Compare September 1, 2026 06:04
Two of Bundle G's seven members. The other five were read and did not land;
each verdict is recorded on the row that owns it rather than carried as a
category, per CLOUD-1164's own rule.

`gh-guard` + `gh-guard-check` + `tests/gh-guard.bats` (0.9s, 23 cases). The
campaign's one retirement whose successor was already green: CLOUD-312 made
`PreToolUse` a single entry, the `gh` lifecycle moved into `batten.toml`'s four
`shape` rows, and that file calls them "the ported `gh-guard`" at :274. Nothing
has invoked either program since — absent from `.claude/settings.json`, from
`mise.toml`'s task graph, from `hk.pkl` and from every workflow, their only
executions were their own suite and `mise run mutant`. All 23 cases were
replayed against the compiled binary and the committed table BEFORE anything was
deleted: 24 of 25 command shapes agree exactly, and the twenty-fifth is
`gh pr ready`, allowed by this predicate and refused by `ready-needs-receipts`
doing its own job. That case asserts the rule id rather than the aggregate
verdict, because reading the exit code would report one rule's correct arrival
as this rule's regression.

`memories-check` + `tests/memories-check.bats` (3.8s, 11 cases) onto
`policy/memories.rego`. Its own header already claimed to be "a pure function of
the tracked tree", and it held: `input.tree.tracked` answers which memories
exist, `input.tree.lines` answers who references one. The reference grammar the
shell restated internally — its comment said the syntax lived "HERE AND ONLY
HERE" — is now two `[[pattern]]` rows, so the one-spelling property is
structural rather than an instruction to the next author: an inline regex is
refused at load, and the module cannot re-spell either token even by accident.

THE PORT FOUND A DOC/MECHANISM DRIFT AND IT IS CORRECTED HERE RATHER THAN
CARRIED FORWARD. `.claude/rules/toolchain.md` claimed `BATTEN_GH_GUARD_BYPASS`
was "the `bypass_env` these rows DECLARE rather than the engine's global hatch".
It is not, and `batten.toml:273` says so in as many words: these four "WOULD
DECLARE `bypass_env = "BATTEN_GH_GUARD_BYPASS"` AND DO NOT YET", deferred to
CLOUD-1027 because adding the key reads to `config-lint` as
`rule-predicate-changed` and owes a groomed `Weakens:` clause first. The test was
written against the rules file, went red against the tree, and the tree was
right. Two cases now pin both directions.

That also settles what this change does NOT conserve. `gh-guard.sh:16` honoured
`BATTEN_GH_GUARD_BYPASS`, and nothing has invoked that program since CLOUD-312,
so the variable has been inert for that whole span. Deleting the file removes
dead code that still appeared to offer a hatch; it does not remove one.
`rules-drift` gates values `.claude/rules/*.md` restates against the mechanisms
that own them, and it did not catch this.

Four file arms and 34 case arms, `$MUTANT_GATES` down by two, and both modules
carry a `#MUTANT-EXEMPT` naming CLOUD-1267 — `mutant` resolves a gate's suite as
`tests/$gate.bats` and `V-SHELL-RULE-ADDED` refuses adding one, so a re-homed
mutation has no named case to turn red and the exemption is the honest record of
that rather than a gap.

ONE STALE REFERENCE IS DELIBERATELY LEFT. `.github/workflows/ci.yml:217` carries
a comment naming "the gh-guard program" as a runtime user of `jq`. The file is
`protected`, the refusal's own remedy is "change it in a pull request", and
spending the hatch on a comment is not worth the precedent. `jq` is still needed
by the remaining shell gates, so the pin is unaffected.

THE CORPUS IS REGENERATED IN THIS COMMIT rather than a following one, because
`suite-bench-check` runs in the pre-commit gate and is bidirectional set
equality against `bench/suites/RESULTS.md` -- a deletion and its corpus cannot
be separated without the intervening commit being red. Regenerated per the
file's own instruction (`mise run test:bats`, then `mise run suite-bench
--write`), never hand-edited.

136 suites, 968.7s serial on this container.

THE SAVING IS 4.7s AND NOT THE 128.4s THE TOTALS DIFFER BY. The previous corpus
recorded 1097.1s on a different run, and subtracting across runs is the error
`ab6efb9` corrected in its own message: the two numbers never shared a baseline.
The defensible figure is the two suites' own share of the corpus they were
measured in -- `tests/gh-guard.bats` 0.9s and `tests/memories-check.bats` 3.8s
of 1097.1s, so 0.43%. The rest of the delta is machine noise and is not claimed.

Refs: CLOUD-1163
Refs: CLOUD-1164

Admits: 9029ddfc6fd45a07a78a5a2eca89d5a78f64e6c9b7940ccd888ccdab07425262
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bb9e875
Admits-epoch: a5e71ccbe2c342c3d9c2ca4939939d9d69e693944afa7b19906f5f044daee2d6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The gh-guard, memories-check and rules-drift programs stay alive as a second implementation of predicates the engine already owns or is taking over, and CLOUD-843 cannot retire any gate at all -- every retirement in the campaign needs its rows here, so refusing this write refuses the campaign rather than this change.
Admits-answer-precondition: A retirement under CLOUD-843 moves a predicate from a shell program into a policy module, and a policy module is unreachable until batten.toml declares its [[rule]] row, its [[pattern]] rows and every [[verdict]] token it can raise -- the engine refuses at LOAD a module raising a token no row declares. No other surface can add those rows: they are the config that makes the module exist. The write is additive blocks appended to the file, visible in the diff of PR #796, 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 rule declaration; there is no other surface that owns it, and the rule this change adds cannot be declared anywhere else. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to be undone -- restoring the file would delete the rows the new module needs to load.
CLOUD-843 bundle G, member 6 (CLOUD-1150). `mise-tasks/rules-drift.sh` and
`tests/rules-drift.bats` are deleted; all four predicates land as
`policy/rules-drift.rego` with `crates/batten/tests/rules_drift.rs` as the
compiled-binary tier. The `$MUTANT_GATES` entry is dropped and `hk.pkl`'s
step keeps its name through a `[tasks.rules-drift]` wrapper, so no caller
moved.

Ledger: two deleted paths, thirty deleted `@test` cases. Twenty-five are
carried; six are `changed:` and say why.

Four anti-vacuity guards change shape together. The predecessor was a TASK
with a call site and refused at startup when an authority would not read; a
`[[rule]]` has no call site, so an unconditional guard would speak in every
fixture repository inheriting this config -- the scoping defect CLOUD-1164
records for `tree-clean`. Each is now conditioned on some prose actually
claiming against the authority.

Two engine limits shaped the module and are recorded rather than absorbed:

* regorus carries no `walk`, so predicate 3's recursive descent became one
  fixed path. Measured, each generated schema carries exactly one such node.
  That buys a vacuity hole a moved schema would fall into silently, so
  `schema_vacuous` covers the read-but-empty case.
* An unreadable declared source does not reach `input.tree.missing` -- it
  stops the WHOLE RULE evaluating, silencing predicates that never read it.
  So the row declares its JSON authorities under `sources` (the glob form),
  which keeps the rule alive across an absent file and lets the module raise
  its own could-not-look. Nothing recovers the unparseable case; the
  predecessor caught it and this does not.

Also here, both found by running the gates over this diff:

* `crates/batten/tests/staged_facts.rs` gains the measurement that keeps
  `lock-complete` out of this bundle. `Format::for_path` searches
  `Format::extensions()` and no variant owns `lock`, so a declared `.lock`
  is `UnknownFormat` before a byte is parsed even though its staged bytes
  are valid TOML -- and `rules.rs:7129` pushes the cause into `missing`,
  where it does not arrive. The loss is downstream of the push.
* `crates/batten/tests/gh_guard.rs` no longer names where this repository is
  hosted (`no-origin-literal-in-fixtures`), rewritten rather than exempted.

`.claude/rules/policy-modules.md` told authors CLOUD-1049 had shipped and the
could-not-look channel worked. It has not; the measured three-way table
replaces that claim, and CLOUD-1049 is reopened with the reproduction.

Refs: CLOUD-843, CLOUD-1150, CLOUD-1163, CLOUD-1164, CLOUD-1049

Admits: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bb9e875
Admits-epoch: a5e71ccbe2c342c3d9c2ca4939939d9d69e693944afa7b19906f5f044daee2d6
Admits-author: alec@wenzowski.com
Admits-prev: 9029ddfc6fd45a07a78a5a2eca89d5a78f64e6c9b7940ccd888ccdab07425262
Admits-answer-lost: mise-tasks/rules-drift.sh cannot retire, so CLOUD-1150 stays open and the shell layer CLOUD-843 exists to retire keeps this gate. The predicates would have to stay in bash, where shell-retirement refuses editing them and the second test tier that found this PR's engine defect does not exist.
Admits-answer-precondition: This commit adds policy/rules-drift.rego, and a module is unreachable until batten.toml declares its [[rule]] row, the five [[pattern]] rows its predicates read, and every [[verdict]] token it can raise -- the engine refuses at LOAD a module raising a token no row declares, and refuses an inline regex so the patterns cannot live in the module either. batten.toml is the only surface that owns those declarations. The write is additive blocks at the end of the file, in the diff of PR #796 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 [[rule]], [[pattern]] and [[verdict]] declaration -- there is nowhere else these can be written, and policy-modules.md refuses the inline alternative for the patterns specifically. R-RESTORE-IT does not apply because the write is the deliberate content of the change rather than damage: restoring the file would delete the rows the new module needs to load, leaving a module that loads and decides nothing.
…appers

`memories-check` and `rules-drift` are now `batten check --rule <id>` behind a
`[tasks.*]` wrapper, and both called the binary on PATH. That judges whatever
happens to be installed rather than the tree being committed.

Measured: lap 1 of `land` rebased onto 1a79d8a, which brought `main`'s new
`[[mcp.source]]` config section under the branch. The installed binary predated
it, so BOTH wrappers refused with a TOML parse error naming a section neither
gate reads -- and the failure looks like a gate regression rather than a stale
binary. Nothing in CI installs `batten` either, so the bare form had never been
CI-verified on any lane.

`cargo run` instead, on the precedent this repository already sets twice:
`batten-check` uses it so the gate always judges the built tree, and
`policy-budget` uses it in the same fast pre-commit tier, so the cost is one
already paid rather than a new one.

Refs: CLOUD-843, CLOUD-1150, CLOUD-1163
The rebase onto `main` put two whole-file regenerations of a generated
artifact against each other: `main`'s (post-bundle-B) still listed the three
suites this branch deletes, and this branch's still listed the three bundle B
deleted. Neither side is true after the merge, and the file's own header says
not to hand-edit it -- so `main`'s was taken as a placeholder through the
conflict and this is the regeneration that replaces it.

135 -> 132 suites, 1008.5s serial, from a full `test:bats` over the merged
tree rather than from either parent's numbers.

Its own commit rather than folded into the retirements, because no single
corpus is correct at all three: commit 1 leaves 133 suites tracked, commit 2
leaves 132. A file regenerated once can only be true at one of them, and head
is where `suite-bench-check`, CI and the fast-forward all read it.

Refs: CLOUD-843, CLOUD-1163
@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 09:18
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 26ae231 into main Sep 1, 2026
19 checks passed
@wenzowski
wenzowski deleted the claude/cloud-843-bundle-g-yvi8p9 branch September 1, 2026 09:35
wenzowski added a commit that referenced this pull request Sep 1, 2026
…rdict

CLOUD-843 bundle G, member 5 -- the punt #796 recorded as "real work, not a
wrapper repoint". It was real work, and it is done rather than deferred.

`stop_nudges` spawned `mise-tasks/unlanded-check.sh` with EMPTY stdin and no
arguments, and the program shelled straight back to `batten state list` and
re-parsed the pointer lines the same binary had written moments earlier -- it
read the PLAIN listing rather than `-J` because a by-path hook gets no mise env
and so no pinned `jq`. `unlanded_pointer` reads the records in process, so there
is no listing to parse and no `jq` to want, and the rule id comes from
`completion::RULE_ID` rather than a shell `RULE_ID` re-typed beside it.

THE READER IS KEYED ON THE REPO ROOT, and getting that wrong is how the port
first failed: `run_state_record` -- which mints the verdict two lines earlier --
keys the store on `git::repo_root`, so a reader anchored on the hook's authority
root looked in a store nothing had written to and reported silence. Measured on
a by-hand fixture whose `batten state list` showed the finding while the hook
stayed quiet, which is the false green this bundle is about, arriving inside the
fix for it.

The two cases covering this rule STUBBED a shell script -- planting a fake
`unlanded-check.sh` that exited 1 and printed a pointer. They therefore asserted
that a non-zero exit from an arbitrary program reaches the channel, which is true
and is not what rule 4 decides; the predicate went untested. Both now drive real
repositories differing in ONE fact -- whether the branch is ahead of
`must_land_on` -- with nothing stubbed, so `record_state` mints and
`unlanded_pointer` reads, and a break in either half reds the case.

Ledger: two deleted paths, twelve deleted cases. Ten carried, two `changed:`.
Both changes are one shape -- a guard the shell needed that the successor cannot
be in: "no binary is silence" is unreachable when the reader IS the binary, and
"a line the reader cannot parse" has no line. The fail-closed direction they
protected survives as the `Observed(count) if count > 0` arm, where `skipped`
and `errored` are still not findings.

Three gates corrected this and each was right. `bats-tests-not-deleted` refused
arms naming paraphrased case titles, then refused `"the bypass is honoured"`
unqualified because `stop-guard.bats` carried a case of that name too and an
unqualified arm cannot say which it accounts for -- the suite-qualified spelling
was already the convention one ledger down. `suite-bench-check` then refused a
corpus still pricing the deleted suite, a cost attached to nothing.

3614 cargo tests, 131 suites, batten-check clean.

Refs: CLOUD-843, CLOUD-1163, CLOUD-97
wenzowski added a commit that referenced this pull request Sep 1, 2026
…rdict

CLOUD-843 bundle G, member 5 -- the punt #796 recorded as "real work, not a
wrapper repoint". It was real work, and it is done rather than deferred.

`stop_nudges` spawned `mise-tasks/unlanded-check.sh` with EMPTY stdin and no
arguments, and the program shelled straight back to `batten state list` and
re-parsed the pointer lines the same binary had written moments earlier -- it
read the PLAIN listing rather than `-J` because a by-path hook gets no mise env
and so no pinned `jq`. `unlanded_pointer` reads the records in process, so there
is no listing to parse and no `jq` to want, and the rule id comes from
`completion::RULE_ID` rather than a shell `RULE_ID` re-typed beside it.

THE READER IS KEYED ON THE REPO ROOT, and getting that wrong is how the port
first failed: `run_state_record` -- which mints the verdict two lines earlier --
keys the store on `git::repo_root`, so a reader anchored on the hook's authority
root looked in a store nothing had written to and reported silence. Measured on
a by-hand fixture whose `batten state list` showed the finding while the hook
stayed quiet, which is the false green this bundle is about, arriving inside the
fix for it.

The two cases covering this rule STUBBED a shell script -- planting a fake
`unlanded-check.sh` that exited 1 and printed a pointer. They therefore asserted
that a non-zero exit from an arbitrary program reaches the channel, which is true
and is not what rule 4 decides; the predicate went untested. Both now drive real
repositories differing in ONE fact -- whether the branch is ahead of
`must_land_on` -- with nothing stubbed, so `record_state` mints and
`unlanded_pointer` reads, and a break in either half reds the case.

Ledger: two deleted paths, twelve deleted cases. Ten carried, two `changed:`.
Both changes are one shape -- a guard the shell needed that the successor cannot
be in: "no binary is silence" is unreachable when the reader IS the binary, and
"a line the reader cannot parse" has no line. The fail-closed direction they
protected survives as the `Observed(count) if count > 0` arm, where `skipped`
and `errored` are still not findings.

Three gates corrected this and each was right. `bats-tests-not-deleted` refused
arms naming paraphrased case titles, then refused `"the bypass is honoured"`
unqualified because `stop-guard.bats` carried a case of that name too and an
unqualified arm cannot say which it accounts for -- the suite-qualified spelling
was already the convention one ledger down. `suite-bench-check` then refused a
corpus still pricing the deleted suite, a cost attached to nothing.

3614 cargo tests, 131 suites, batten-check clean.

Refs: CLOUD-843, CLOUD-1163, CLOUD-97
wenzowski added a commit that referenced this pull request Sep 1, 2026
…rdict

CLOUD-843 bundle G, member 5 -- the punt #796 recorded as "real work, not a
wrapper repoint". It was real work, and it is done rather than deferred.

`stop_nudges` spawned `mise-tasks/unlanded-check.sh` with EMPTY stdin and no
arguments, and the program shelled straight back to `batten state list` and
re-parsed the pointer lines the same binary had written moments earlier -- it
read the PLAIN listing rather than `-J` because a by-path hook gets no mise env
and so no pinned `jq`. `unlanded_pointer` reads the records in process, so there
is no listing to parse and no `jq` to want, and the rule id comes from
`completion::RULE_ID` rather than a shell `RULE_ID` re-typed beside it.

THE READER IS KEYED ON THE REPO ROOT, and getting that wrong is how the port
first failed: `run_state_record` -- which mints the verdict two lines earlier --
keys the store on `git::repo_root`, so a reader anchored on the hook's authority
root looked in a store nothing had written to and reported silence. Measured on
a by-hand fixture whose `batten state list` showed the finding while the hook
stayed quiet, which is the false green this bundle is about, arriving inside the
fix for it.

The two cases covering this rule STUBBED a shell script -- planting a fake
`unlanded-check.sh` that exited 1 and printed a pointer. They therefore asserted
that a non-zero exit from an arbitrary program reaches the channel, which is true
and is not what rule 4 decides; the predicate went untested. Both now drive real
repositories differing in ONE fact -- whether the branch is ahead of
`must_land_on` -- with nothing stubbed, so `record_state` mints and
`unlanded_pointer` reads, and a break in either half reds the case.

Ledger: two deleted paths, twelve deleted cases. Ten carried, two `changed:`.
Both changes are one shape -- a guard the shell needed that the successor cannot
be in: "no binary is silence" is unreachable when the reader IS the binary, and
"a line the reader cannot parse" has no line. The fail-closed direction they
protected survives as the `Observed(count) if count > 0` arm, where `skipped`
and `errored` are still not findings.

Three gates corrected this and each was right. `bats-tests-not-deleted` refused
arms naming paraphrased case titles, then refused `"the bypass is honoured"`
unqualified because `stop-guard.bats` carried a case of that name too and an
unqualified arm cannot say which it accounts for -- the suite-qualified spelling
was already the convention one ledger down. `suite-bench-check` then refused a
corpus still pricing the deleted suite, a cost attached to nothing.

3614 cargo tests, 131 suites, batten-check clean.

Refs: CLOUD-843, CLOUD-1163, CLOUD-97
wenzowski added a commit that referenced this pull request Sep 1, 2026
…rdict

CLOUD-843 bundle G, member 5 -- the punt #796 recorded as "real work, not a
wrapper repoint". It was real work, and it is done rather than deferred.

`stop_nudges` spawned `mise-tasks/unlanded-check.sh` with EMPTY stdin and no
arguments, and the program shelled straight back to `batten state list` and
re-parsed the pointer lines the same binary had written moments earlier -- it
read the PLAIN listing rather than `-J` because a by-path hook gets no mise env
and so no pinned `jq`. `unlanded_pointer` reads the records in process, so there
is no listing to parse and no `jq` to want, and the rule id comes from
`completion::RULE_ID` rather than a shell `RULE_ID` re-typed beside it.

THE READER IS KEYED ON THE REPO ROOT, and getting that wrong is how the port
first failed: `run_state_record` -- which mints the verdict two lines earlier --
keys the store on `git::repo_root`, so a reader anchored on the hook's authority
root looked in a store nothing had written to and reported silence. Measured on
a by-hand fixture whose `batten state list` showed the finding while the hook
stayed quiet, which is the false green this bundle is about, arriving inside the
fix for it.

The two cases covering this rule STUBBED a shell script -- planting a fake
`unlanded-check.sh` that exited 1 and printed a pointer. They therefore asserted
that a non-zero exit from an arbitrary program reaches the channel, which is true
and is not what rule 4 decides; the predicate went untested. Both now drive real
repositories differing in ONE fact -- whether the branch is ahead of
`must_land_on` -- with nothing stubbed, so `record_state` mints and
`unlanded_pointer` reads, and a break in either half reds the case.

Ledger: two deleted paths, twelve deleted cases. Ten carried, two `changed:`.
Both changes are one shape -- a guard the shell needed that the successor cannot
be in: "no binary is silence" is unreachable when the reader IS the binary, and
"a line the reader cannot parse" has no line. The fail-closed direction they
protected survives as the `Observed(count) if count > 0` arm, where `skipped`
and `errored` are still not findings.

Three gates corrected this and each was right. `bats-tests-not-deleted` refused
arms naming paraphrased case titles, then refused `"the bypass is honoured"`
unqualified because `stop-guard.bats` carried a case of that name too and an
unqualified arm cannot say which it accounts for -- the suite-qualified spelling
was already the convention one ledger down. `suite-bench-check` then refused a
corpus still pricing the deleted suite, a cost attached to nothing.

3614 cargo tests, 131 suites, batten-check clean.

Refs: CLOUD-843, CLOUD-1163, CLOUD-97
wenzowski added a commit that referenced this pull request Sep 1, 2026
…rdict

CLOUD-843 bundle G, member 5 -- the punt #796 recorded as "real work, not a
wrapper repoint". It was real work, and it is done rather than deferred.

`stop_nudges` spawned `mise-tasks/unlanded-check.sh` with EMPTY stdin and no
arguments, and the program shelled straight back to `batten state list` and
re-parsed the pointer lines the same binary had written moments earlier -- it
read the PLAIN listing rather than `-J` because a by-path hook gets no mise env
and so no pinned `jq`. `unlanded_pointer` reads the records in process, so there
is no listing to parse and no `jq` to want, and the rule id comes from
`completion::RULE_ID` rather than a shell `RULE_ID` re-typed beside it.

THE READER IS KEYED ON THE REPO ROOT, and getting that wrong is how the port
first failed: `run_state_record` -- which mints the verdict two lines earlier --
keys the store on `git::repo_root`, so a reader anchored on the hook's authority
root looked in a store nothing had written to and reported silence. Measured on
a by-hand fixture whose `batten state list` showed the finding while the hook
stayed quiet, which is the false green this bundle is about, arriving inside the
fix for it.

The two cases covering this rule STUBBED a shell script -- planting a fake
`unlanded-check.sh` that exited 1 and printed a pointer. They therefore asserted
that a non-zero exit from an arbitrary program reaches the channel, which is true
and is not what rule 4 decides; the predicate went untested. Both now drive real
repositories differing in ONE fact -- whether the branch is ahead of
`must_land_on` -- with nothing stubbed, so `record_state` mints and
`unlanded_pointer` reads, and a break in either half reds the case.

Ledger: two deleted paths, twelve deleted cases. Ten carried, two `changed:`.
Both changes are one shape -- a guard the shell needed that the successor cannot
be in: "no binary is silence" is unreachable when the reader IS the binary, and
"a line the reader cannot parse" has no line. The fail-closed direction they
protected survives as the `Observed(count) if count > 0` arm, where `skipped`
and `errored` are still not findings.

Three gates corrected this and each was right. `bats-tests-not-deleted` refused
arms naming paraphrased case titles, then refused `"the bypass is honoured"`
unqualified because `stop-guard.bats` carried a case of that name too and an
unqualified arm cannot say which it accounts for -- the suite-qualified spelling
was already the convention one ledger down. `suite-bench-check` then refused a
corpus still pricing the deleted suite, a cost attached to nothing.

3614 cargo tests, 131 suites, batten-check clean.

Refs: CLOUD-843, CLOUD-1163, CLOUD-97
wenzowski added a commit that referenced this pull request Sep 1, 2026
…rdict

CLOUD-843 bundle G, member 5 -- the punt #796 recorded as "real work, not a
wrapper repoint". It was real work, and it is done rather than deferred.

`stop_nudges` spawned `mise-tasks/unlanded-check.sh` with EMPTY stdin and no
arguments, and the program shelled straight back to `batten state list` and
re-parsed the pointer lines the same binary had written moments earlier -- it
read the PLAIN listing rather than `-J` because a by-path hook gets no mise env
and so no pinned `jq`. `unlanded_pointer` reads the records in process, so there
is no listing to parse and no `jq` to want, and the rule id comes from
`completion::RULE_ID` rather than a shell `RULE_ID` re-typed beside it.

THE READER IS KEYED ON THE REPO ROOT, and getting that wrong is how the port
first failed: `run_state_record` -- which mints the verdict two lines earlier --
keys the store on `git::repo_root`, so a reader anchored on the hook's authority
root looked in a store nothing had written to and reported silence. Measured on
a by-hand fixture whose `batten state list` showed the finding while the hook
stayed quiet, which is the false green this bundle is about, arriving inside the
fix for it.

The two cases covering this rule STUBBED a shell script -- planting a fake
`unlanded-check.sh` that exited 1 and printed a pointer. They therefore asserted
that a non-zero exit from an arbitrary program reaches the channel, which is true
and is not what rule 4 decides; the predicate went untested. Both now drive real
repositories differing in ONE fact -- whether the branch is ahead of
`must_land_on` -- with nothing stubbed, so `record_state` mints and
`unlanded_pointer` reads, and a break in either half reds the case.

Ledger: two deleted paths, twelve deleted cases. Ten carried, two `changed:`.
Both changes are one shape -- a guard the shell needed that the successor cannot
be in: "no binary is silence" is unreachable when the reader IS the binary, and
"a line the reader cannot parse" has no line. The fail-closed direction they
protected survives as the `Observed(count) if count > 0` arm, where `skipped`
and `errored` are still not findings.

Three gates corrected this and each was right. `bats-tests-not-deleted` refused
arms naming paraphrased case titles, then refused `"the bypass is honoured"`
unqualified because `stop-guard.bats` carried a case of that name too and an
unqualified arm cannot say which it accounts for -- the suite-qualified spelling
was already the convention one ledger down. `suite-bench-check` then refused a
corpus still pricing the deleted suite, a cost attached to nothing.

3614 cargo tests, 131 suites, batten-check clean.

Refs: CLOUD-843, CLOUD-1163, CLOUD-97
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant