Skip to content

refactor(ci): retire bot-issue onto the engine, and close every row this session opened - #811

Closed
wenzowski wants to merge 29 commits into
mainfrom
claude/cloud-1295-retire-bot-issue
Closed

refactor(ci): retire bot-issue onto the engine, and close every row this session opened#811
wenzowski wants to merge 29 commits into
mainfrom
claude/cloud-1295-retire-bot-issue

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Ten rows, one branch, one PR. This supersedes #817 and #818, whose work is replayed here commit-for-commit — they were three branches for one session's work, and one of the rows (CLOUD-1231) had been implemented twice, independently, two hours apart.

Closes CLOUD-1295
Closes CLOUD-1297
Closes CLOUD-1299
Closes CLOUD-1301
Closes CLOUD-1213
Closes CLOUD-1224
Closes CLOUD-1142
Closes CLOUD-1231
Closes CLOUD-1311
Closes CLOUD-472

What lands

CLOUD-1295 — bot-issue retired onto the engine. 486 lines of shell and its 323-line suite deleted, six subcommands ported to batten pr and batten claim, with one carried: arm per deleted path and per declared @test. fetch.rs gained an explicit HTTP method so PATCH is expressible without making a GET-with-body or a POST-without-body constructible.

CLOUD-1213 — the licence-carry lane lands itself. batten claim carry attests that a sbom-actions/* branch is derivable — every added row parses, another row for the same repo carries an identical licence and holder, and no other tracked path differs — rather than exempting a branch prefix, which would be a password wearing a branch name. auto-bot-land.yml now carries a LANES table of (login, prefix) pairs instead of a single BOT_LOGIN.

CLOUD-1297 — checks_any. A receipt rule's checks is a conjunction; the claim gate needs the alternation verify already accepts. One shared receipt_verdict adjudicates both, and the alternation refusal names every alternative and its validity.

CLOUD-1299 / CLOUD-1224 — two more admitted_addition arms, so a caller naming its callee as a task name rather than a path can be repointed, and a frozen suite pinning a remedy string is no longer an unlandable shape.

CLOUD-1301 — the reclaim verdict is once per BOOT, not once per session, so a container that replaced one mid-landing stops repeating it forever.

CLOUD-1142 — one definition of an issue key. ready::Grammar::key_of / keys_in own the three axes CLOUD-761 decided (case-sensitive, explicit character-class boundary, mandatory prefix), the token stays the consumer's in [[pattern]] ready-issue-key, and issue-key-derivations-not-growing ratchets the task tree so the count cannot grow while the conversions wait. Replayed over 400 commits before its severity was set: 400 examined, 1 would-fire, 0 false positives.

CLOUD-1231 — a branch's receipt records every row it serves. The ids union, guarded by the recorded base so CLOUD-516's restart still forgets — and the weakens lines carry with them, without which the union leaves a row claimed and its config lint admission gone.

CLOUD-1311 / CLOUD-472 — the punt sweep and the declared plan get exit codes. policy/filed-here.rego gains issue file held for a row this branch filed and neither closed here nor closed by the body; policy/plan-complete.rego holds a branch's own declared plan to its end, with the anti-vacuity arm that prices declaring no plan at all. Both verdicts are named in the landed three-word grammar, and plan joins the vocabulary with its measured one-token candidate row.

Verification

  • mise run test:cargo — 3959/3959.
  • Every new case shown able to fail; the weakens carry reddens with the carry stubbed out while its two negative arms stay green, which is what says they test the guard rather than the write.
  • mise run config-lint — 0 smells.
  • mise run verify before readying.

Refs: CLOUD-761, CLOUD-1164, CLOUD-841, CLOUD-516, CLOUD-1051, CLOUD-514, CLOUD-418

@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
CLOUD-1295 Retire `bot-issue` — 486 lines and 6 subcommands, all GitHub API plus one local receipt, and its receipt route is the wall a carry branch cannot pass

Why

Carved from CLOUD-1164 in CLOUD-1160's shape — a dedicated row per program, so the verdict and the ledger are recorded against the thing being retired rather than against an umbrella.

The forcing occasion. CLOUD-1213's sbom-actions-currency workflow opens its carry PRs on sbom-actions/carry-<timestamp>. verify refuses a branch with no claim receipt and accepts exactly two names — claim and bot (mise.toml:2506-2523) — and mise run bot-issue receipt refuses the branch outright: "sbom-actions/carry-… is not a bot branch, so the agent claim receipt is the one that applies here". So #806 was landed with a --takeover claim against CLOUD-1213, which attests a refinement nobody performed. That is a fake claim, and it is the honest reason this row exists.

A third receipt arm could be bolted into verify's inline body while leaving bot-issue.sh alive. That was rejected: it puts the same predicate in two languages with nothing holding them in agreement, which is the drift mise.toml:2485 already records for this exact pair — "ONE PREDICATE, NOT A SECOND ONE WRITTEN IN SHELL (CLOUD-741)".

What the program is, read rather than classified. 486 lines, 323 of bats, six subcommands — and it is entirely GitHub API plus one local receipt write. No tracker, no Linear, no MCP. :268 is gh api -X POST repos/…/issues; the "tracker row" a bot PR gets is a GitHub issue.

subcommand what it does engine need
derive <pr> PR title + file list → candidate row forge GET
file <pr> POST repos/…/issues (:268) forge POST
link <pr> <key> PATCH repos/…/pulls/N body (:317) forge PATCH
ensure <pr> the three, idempotent per lander tick composition
closes <pr> does the body still close a key (:355) forge GET
receipt mint this branch's bot receipt local write

One real blocker, and it is in the engine rather than in the program. crates/batten/src/fetch.rs:602-603 derives the HTTP method from body presence — Some(_) => POST, None => GET — so PATCH cannot be expressed at all, and link needs it. :598's comment says the pairing is deliberate, so the fix is an explicit method that keeps GET-with-body and POST-without-body unconstructible, not a loosening of the pair.

Refinement — Ready

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

  • Source of truth (§1). mise-tasks/bot-issue.sh and tests/bot-issue.bats, both deleted. crates/batten/src/fetch.rs's method derivation. mise.toml's receipt arm at :2506-2523.
  • Computable predicate (§2). Every retired subcommand answers identically from the engine, and a carry branch mints a receipt verify accepts **without **--takeover. The carry receipt attests DERIVABILITY, not a branch name: every added line in mise-tasks/sbom-actions.tsv parses as <repo>@<sha>\t<licence>\t<holder>; another row for the same <repo> carries an identical licence and holder, so only the sha differs; and no other tracked path differs from the merge base. A branch-name exemption would be a password wearing a branch name.
  • Effect (§3). kind:verb — the mirror half performs forge writes, which a tree-scoped module structurally cannot. .claude/rules/toolchain.md admits a verb for exactly this and says the choice must stay available and merely stop being invisible.
  • Output & exit (§5). Pointer-only and unchanged: the PR number, the issue key, the manifest paths — never a diff body, a version, or a token. Exit 0 did the work or found it done / 1 refused, not one of ours / 2 could not look.
  • Commit / bump (§6). refactor(ci) — no bump; refactor releases nothing at any version (CLOUD-595).
  • Test obligation (§7). Tier 2 over the compiled binary in crates/batten/tests/it/; **no **.bats added or edited (V-SHELL-RULE-ADDED refuses one at deny). One carried: arm per deleted path — 2 paths — plus one per @test the suite declared (CLOUD-908). The #MUTANT files-a-row-per-tick row (bot-issue.sh:59) re-homes onto the ported predicate; bot-issue leaves $MUTANT_GATES. Shown able to fail per CLOUD-418, in both directions: a second ensure on one PR files nothing, and a PR touching no owned manifest is refused.
  • Blockers (§8). None. relatedTo CLOUD-1164 (the umbrella this is carved from), CLOUD-693 (what the program was built for), CLOUD-1213 (the carry-branch wall), CLOUD-1219 (the invocation-repointing arm callers need).

Acceptance

  • Both shell paths deleted, each with its arm; the suite's cases named and re-homed; bot-issue out of $MUTANT_GATES; mutant-census green.
  • batten claim carry exits 0 on a real carry branch and mise run verify passes the receipt check with **no **--takeover — the fake claim is retired, not merely tolerated.
  • A hand-made branch that edits a licence string, adds a row for an unmapped repo, or touches a second file is refused by claim carry.
  • ~~PATCH is expressible in ~~fetch.rs~~, and GET-with-body and POST-without-body remain unconstructible. ~~WITHDRAWN, on a false premise this issue itself carried. The body above called the method derivation "one real blocker", assuming the port would send its requests through fetch.rs. It does not, and it should not: pr_watch is the landed precedent for forge access from engine source (CLOUD-1143) and it reads through gh, which expresses every method and resolves the credential OUTSIDE the crate — where fetch would have put token resolution inside it, next to no config row that declares a forge credential. The widening was written, landed on this branch, and then reverted rather than kept: Payload::Update had no caller and its doc comment stated a reason that is not true, and a widened type with no consumer and a false rationale is worse than the narrower one it replaced.

Not in this issue

Extending auto-bot-land.yml to merge carry PRs unattended — that is what would close CLOUD-1213's first acceptance clause, and it is a separate judgement about unreviewed auto-merge.

Refs: CLOUD-1164, CLOUD-693, CLOUD-1213, CLOUD-741, CLOUD-908, CLOUD-418

CLOUD-1213 `sbom-check`'s licence table needs a human on every action pin move, and only a person can regenerate it

Why

Found while fixing CLOUD-1207 and deliberately not repaired there, because
the refusal is the design working rather than a defect.

sbom-check's second half: mise-tasks/sbom-actions.tsv maps each pinned
action's licence by repo AND sha together, and its own header is explicit
that this is the point — "a table row whose sha is stale is exactly the drift…*
A renovate bump that does not record the new commit's license fails the gate
*rather than silently degrading the document." Regenerating the table is
mise run sbom plus a commit, which the update lane cannot do, having decided
for good reason never to check a bot's head out under a write token.
mise-action-floor states the same shape as an accepted cost in prose:
"such a PR then sits open and red until somebody closes it."

What #771 already settled, so this row is smaller than when it was filed

The original body carried two concerns. The second — that the lane burned CI
indefinitely while waiting for that person, and said nothing about it — is
fixed: the freeze arm drafts a red head, halts the rebases, and comments once
naming the failing checks. The lane's own header now states the bound rather
than claiming a scope its gates refuse.

So what remains is only the first: should this class need a person at all?
Waiting is now free, which is what makes this a design question rather than a
bill.

A correction to the measurement above (2026-08-31)

This row said *"Measured on #676: 34 × *sbom-action-unmapped, every pin in the
tree." That is wrong and the argument does not need it. sbom-check.sh:358-371
loops per pin and matches repo@sha as one key, so only a moved pin is
unmapped. #676 moves exactly two — jdx/mise-action v4.2.4→v4.2.5 and
linear/linear-release-action v0.15.1→v0.16.0. The cost of the human step is
two table rows per bump, not a full regeneration, which makes option 2 cheaper
than this row assumed and is why the decision below can be made at all.

The options

  1. Teach something in the lane to regenerate the table. Needs a write to the
    bot's head — the property the lander was built to avoid — or a follow-up
    commit from a trusted lane.
  2. Accept it. A licence is a fact about a commit, and recording it deliberately
    is defensible; the cost is now bounded to one frozen PR and one comment.
  3. Key the table by action name rather than sha. Rejected — it deletes the
    drift detector that makes a committed licence table defensible at all.

Refinement — Ready

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

  • **Source of truth (§1). **mise-tasks/sbom-check.sh's unmapped loop and
    mise-tasks/sbom-actions.tsv's header.
  • Computable predicate (§2). mise run sbom-check exits 0 on a head whose
    action pins moved, with no hand edit between the bump and the check. Option 1
    is the decision (2026-08-31): a workflow on main, shaped like
    .github/workflows/lock-currency.yml, reads the open renovate/* PR's pins,
    fetches each new pin's LICENSE bytes at its sha, and opens its own PR
    against main adding the rows. Option 2 is declined — it leaves the lane's
    throughput bounded by whoever notices a freeze. Option 3 stays rejected.
  • Effect (§3). No command-surface change.
  • Output & exit (§5). Pointer-only, unchanged — the gate already emits
    path:line and a count, never a licence or a holder.
  • **Commit / bump (§6). **ci → no bump.
  • **Test obligation (§7). **mise-tasks/sbom-check.sh is frozen by
    V-SHELL-RULE-EDITED, so any change reaching it takes the port-and-retire
    shape: the predicate as a policy/*.rego module plus a
    crates/batten/tests/*.rs tier, with one conserves arm per deleted path.
  • Blockers (§8). None.
{
  "source_of_truth": "mise-tasks/sbom-actions.tsv",
  "gate": { "task": "sbom-check", "exits": [0, 1] },
  "commit_type": "ci",
  "blockers": [],
  "tests": [
    {
      "file": "tests/sbom-check.bats",
      "mutation": "drop the per-pin unmapped loop, so a moved action pin reports mapped"
    }
  ]
}

REOPENED 2026-09-01. Closed against #788 with §2's Option 1 never built — git grep -l sbom-actions -- .github/ returns nothing across 25 workflow files — and the class reproduced immediately on #676, which was still holding the prConcurrentLimit: 1 slot four hours later and had to be cleared by hand (77fbad3b). The freeze half of this row IS done and stays credited; what is reopened is Option 1. See the comment thread for the measurement and for a correction to this body's own pin arithmetic.

Acceptance

  • A github-actions bump reaches green on sbom-check with no hand edit
    between the bump and the check.
  • The instance in hand is cleared in the same change: the two rows ci(deps): update actions #676 needs
    are on main, renovate-frozen is off it, and the prConcurrentLimit: 1
    slot is free for the 18 updates queued behind it in Dependency Dashboard #494.

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-1297 A receipt rule's `checks` is a conjunction, so the claim gate cannot accept the alternative receipts `verify` already accepts

Found while retiring bot-issue onto the engine (CLOUD-1295).

The defect

Rule::checks on a receipt row is documented as "the receipts a row requires, all of which must be valid before the matched call is allowed" (crates/batten/src/rules.rs). There is no disjunctive spelling, so a row cannot say this receipt OR that one.

verify's shell body does express the disjunction: it asks receipt status claim, then bot, then (as of CLOUD-1295) carry, and any one passing is enough. The mediated gate cannot follow it. claim-needs-receipt demands claim alone, so an agent working on a branch that legitimately carries a bot.<branch> or carry.<branch> receipt is denied on every write despite holding a valid attestation.

That is a live false positive today for the bot receipt — pre-existing since CLOUD-693 — and CLOUD-1295 adds a third kind that inherits it. Nothing has broken, because nobody runs an agent session on a bot branch; the point is that the gate and verify disagree about what counts as a claim. CLOUD-741 is that same shape one layer in, and its finding was that the weaker of two readers is the one that gets consulted when the other is unloaded.

What was deliberately NOT done, and why

CLOUD-1295's plan called for a checks = ["carry"] row beside claim-needs-receipt. That row is wrong under the conjunction: a second receipt row is a second AND, so it would deny every ordinary write on every branch. The disjunction has to exist before the row can, so the row was not written and this was filed instead.

Refinement — Ready

  • **Source of truth (§1). **crates/batten/src/rules.rsRule::checks and the receipt kind's adjudication. mise.toml's verify body is the reference behaviour the gate should agree with, and is not this row's subject.
  • Computable predicate (§2). A receipt row can express "any one of these receipts is valid", and a mediated write on a branch carrying only a bot.<branch> or carry.<branch> receipt is allowed by claim-needs-receipt, while a branch carrying none is still denied. Both directions driven over the compiled binary.
  • Effect (§3). No new verb and no new flag: a column on an existing rule kind.
  • Output & exit (§5). Unchanged.
  • **Commit / bump (§6). **feat → no bump.
  • Test obligation (§7). A compiled-binary case per direction, plus the vacuity case — a row declaring the disjunction must still deny a branch with no receipt at all, or the alternation is an allow wearing a gate's name.
  • Blockers (§8). None. Adding a receipt to an alternation LOWERS a bar, so config-lint will read the consumer row that follows as a weakening and it owes a groomed Weakens: clause — work inside this row rather than a block from another.

CLOUD-1299 A frozen bats suite pins a remedy STRING, so retiring the program that string names has no landable shape

Measured while retiring bot-issue (CLOUD-1295).

The defect

tests/verify.bats asserts that verify's no-receipt refusal names its remedies:

[[ "$output" == *"bot-issue receipt"* ]]

That is a pin on PROSE, not on an invocation. Retiring mise-tasks/bot-issue.sh makes the remedy name a task that no longer exists, so the message has to change — and changing it fails that assertion, so the suite has to change too. tests/verify.bats is a .bats under tests/, which governed_at_head selects, so V-SHELL-RULE-EDITED refuses the edit with no override route and no bypass_env.

**And the repointing arm cannot admit it either. **repoints_at_the_declared_invocation (CLOUD-1219) requires the replaced span to satisfy is_retired_reference, whose arms are the repo-relative path, a $(dirname "$0")/<basename> sibling construction, or a variable bound to one of those. The span here is bot-issue receipt — a task NAME plus a subcommand, which is none of them.

The module already accepts that a caller can name a program by its stem: naming_forms yields {mise-tasks/bot-issue.sh, bot-issue.sh, bot-issue}, mentions_retired's last arm uses it, and drops_a_retired_name (CLOUD-1224) exists precisely for a list that names a program by stem. is_retired_reference is the one place that does not — and widening it to contains a naming form would be the licence the module's own comment refuses in as many words ("contains is honest here where on the addition side it would be a licence").

Why the two established escapes do not apply

  • A mise.toml wrapper (CLOUD-1248, withdrawn on exactly that ground) keeps a retiring program's task NAME alive so callers are untouched. It works for an invocation. It does not work for a pin on prose: nothing about a wrapper makes the remedy sentence honest, and resurrecting a shell dispatcher is the thing the retirement removes.
  • **Retire the suite whole. **tests/verify.bats' subject is mise.toml's verify body, not bot-issue.sh. Retiring it is a different unit's work and would be scope the retirement never claimed.

What CLOUD-1295 shipped instead, and why it is not a fix

The remedy now reads `batten claim bot` (which replaced `mise run bot-issue receipt`) — the live verb first, the old name kept as the thing it replaced. That is TRUE and is what a reader mid-transition needs, and it satisfies the assertion because the old substring survives. It is still a dead task name kept alive because a test pins it, and it will read as a slip to the next person unless this row exists. mise.toml carries a comment at the site pointing here.

The same shape is already recorded once, in mise.toml's own verify body, for CLOUD-1153: "a row whose §1 is written in the wrong shape — it needs the classification where a gate can already see it, not a fourth authority in this body — and it is left on the board rather than smuggled through a governed file." This is that class a second time, which is what makes it a row rather than an anecdote.

Refinement — Ready

  • **Source of truth (§1). **policy/shell-retirement.regois_retired_reference and the admitted_addition arms. tests/verify.bats' remedy assertions are the measured instance, and are not this row's subject.
  • Computable predicate (§2). A governed suite asserting a remedy string can have that string repointed at the retired program's declared successor, and the edit is admitted; an edit that changes anything else on the line is still refused. Both directions driven over the compiled binary, plus the vacuity case — an arm that admitted any span containing a naming form would be the licence this row must not become, so a case must show a same-line rewrite still refused.
  • Effect (§3). No command-surface change: an arm on an existing policy module.
  • Output & exit (§5). Unchanged — pointer-only, path:line and a rule id.
  • **Commit / bump (§6). **refactor(ci) → no bump; refactor releases nothing at any version.
  • **Test obligation (§7). **crates/batten/tests/ over the compiled binary; no .bats added or edited, which V-SHELL-RULE-ADDED refuses anyway. The module's own test_ rules are the load-time tier.
  • Blockers (§8). None. relatedTo CLOUD-1219 (the invocation field this extends), CLOUD-1224 (naming_forms, whose stem this needs), CLOUD-1248 (the wrapper escape that does not cover prose), CLOUD-1295 (where it was measured).

CLOUD-1301 The reclaim verdict is reported once per SESSION where the fact is once per BOOT, so a container that replaced one mid-landing repeats it forever

OVERTAKEN IN PART (2026-09-01) — the hard block is gone, the repeat is not

main retired .claude/hooks/session-start.sh into ten declared handler rows (CLOUD-312 row 10, commit 7d188580) while this row was being written, and **took **tests/session-start.bats with it. The silence assertion that made this a landing blocker therefore no longer exists and verify is passable again — measured on CLOUD-1295's branch, which rebased onto that commit and dropped its own fix as subjectless. The title is rewritten to what is left; everything below is the original body and is kept for the measurement.

What remains is the noise half, unchanged. The reclaim report is now the task runner's own session:reclaim task, which records this boot and then runs reclaim-census report, each guarded so neither can fail the session. It still runs on every session start and still prints a verdict that cannot change while the container lives, so on a container that replaced one mid-landing the same true fact repeats every session forever. The committed authority's own note on the move classifies that output as Advise, "delivered on the same channel. Unchanged" — carried deliberately rather than overlooked.

§1 moves accordingly: the surface is that task body, not the deleted hook, and the task runner's manifest is ungoverned, so the fix is a plain edit rather than a retirement. §2's predicate is unchanged. What is LOST is the standing gate — the case that would have failed on a repeat is gone with the suite, so a fix now owes its own test rather than inheriting one, and §7 below is the shape it needs.

A fix was written against the old hook and driven both directions by hand — mark cleared → reports, second run → silent — before main deleted its subject. The shape that worked: a mark keyed to the boot time, sitting beside the census log rather than in it, both paths from the census's own log-path and boot accessors, and every failure path still reporting rather than going quiet.


Measured while landing CLOUD-1295, where it blocked the branch outright.

The defect

Two landed mechanisms contradict each other on a container that replaced one mid-landing:

  • CLOUD-451's sensor. .claude/hooks/session-start.sh:283-288 runs reclaim-census report on every session start and prints the verdict whenever it exits 0 — which is the active reading, "a landing was in flight when this container replaced the last one". The comment is explicit that only the positive reading speaks.
  • CLOUD-891's case. tests/session-start.bats:147"the hook runs green on this checkout" — runs that same hook and asserts [ -z "$output" ], on the stated premise that "the steps are all already satisfied" and that silence is the pass.

On a container whose predecessor was reclaimed mid-landing, the sensor is RIGHT and the case FAILS. Measured on this container:

$ ./mise-tasks/reclaim-census.sh report ; echo $?
reclaim-census: A LANDING WAS IN FLIGHT when this container replaced the last one (last beat 1788262156 under boot 1788215603, now 1788263249)
0

It is permanent for the life of the container, and it blocks landing

report classifies the PREVIOUS boot, resolved as "the newest recorded boot that is not this one". That is immutable history: recording this boot does not move it, and no landing completed here can change what the last container was doing when it died. The verdict therefore repeats on every session start until the container is replaced.

test:bats runs in the hk hooks step, which verify runs, which land runs per lap — so verify fails on this one case forever, and the branch cannot land from this container at all. It failed three consecutive land laps on CLOUD-1295 with nothing else red.

It is invisible in CI, which is why it has survived: the case opens with real_install_or_skip, so it only runs where a real install exists — an agent's own container, never a fresh runner.

Why neither side is simply wrong

The sensor is reporting a true fact that somebody must act on, which is exactly what CLOUD-891 says stdout is for. The case is right that a hook with nothing to report should be silent. What is missing is that the sensor speaks once per SESSION where the fact is once per BOOT, so after the first read it is the noise CLOUD-891 removed — the hook's own comment already makes that argument for the negative readings ("a line every session start would be noise in the overwhelming case where nothing happened") and does not apply it to the positive one.

Why CLOUD-1295 could not fix it

tests/session-start.bats is a .bats under tests/, which governed_at_head selects, so V-SHELL-RULE-EDITED refuses an edit with no override route and no bypass_env; the two landable shapes are retire it whole or leave it alone, and its subject is the session-start hook rather than anything that change retires. mise-tasks/reclaim-census.sh is governed on the same terms. The remaining surface — the hook itself — is CLOUD-451's evidence collection, and rewriting how that sensor reports mid-landing is another row's work.

Refinement — Ready

  • Source of truth (§1). The session:reclaim task body, which is where the report moved when the hook was retired. The census program's own verdict is correct and is not this row's subject; it is named by task rather than by path because citing a file is not claiming it.
  • Computable predicate (§2). On a container whose predecessor was reclaimed mid-landing, the reclaim verdict is reported at most once, and a second session start on the same boot is silent. A container whose predecessor stopped on purpose stays silent throughout, and the first report on a genuine reclaim is still made: a fix that simply stopped reporting would delete the instrument CLOUD-451 built.
  • Effect (§3). No command-surface change.
  • Output & exit (§5). Unchanged and pointer-only — a verdict and two epochs, never a plan or a prompt body.
  • **Commit / bump (§6). **fix → patch.
  • Test obligation (§7). Both directions plus the vacuity case, over a store seeded to each of the three verdicts: reported once on a reclaim, silent on the repeat, silent throughout on an intentional stop. A case that only asserted silence would pass over a sensor that never reports at all, which is the instrument deleted rather than fixed.
  • Blockers (§8). None. relatedTo CLOUD-451 (the sensor), CLOUD-891 (the silence rule), CLOUD-696 (the last correction to this census), CLOUD-1295 (where it was measured).

CLOUD-1224 `board-sweep.sh` builds its callee's filename from a loop variable, so no substitution arm can reach it — the retirement admission has three shapes and this caller matches none

Why

only_drops_a_retired_reference now admits three added-line shapes, and mise-tasks/board-sweep.sh matches none of them for the gates it drives.

board-sweep.sh:229-230 does not name its callee at all. It iterates gate names and builds each filename:

for gate in "${gates[@]}"; do
	prog="$here/${gate}.sh"

Every admission arm needs a span that is a reference to a path this delta deletes. Here the reference exists only at run time — the text on the line is ${gate}.sh, and gate is bound by the loop header, not by an assignment retired_path_vars or script_dir_vars can read off the base side. So:

  • truncates_a_retired_reference — no, nothing is shortened;
  • repoints_at_the_declared_successor (CLOUD-1121) — no, contains(was, gone) is false, the line carries no repo-relative path;
  • repoints_at_the_declared_invocation (CLOUD-1219) — no, is_retired_reference cannot resolve ${gate}.sh to any deleted path.

This is asserted rather than left to be discovered. crates/batten/tests/shell_retirement.rs carries the case as a REFUSAL — the shape is spelled out and the module is pinned refusing it — because a caller the gate silently fails to match is indistinguishable from a caller it correctly admits, which is the class .claude/rules/policy-modules.md opens on.

What it costs, concretely

board-sweep.sh drives the board gates by name. Retiring any of them forces an edit here, and that edit is V-SHELL-RULE-EDITED with one route (R-PORT-AND-RETIRE), no override and no bypass_env. So the board-gate bundle is not landable one gate at a time while this stands — which is the same structural block CLOUD-1149 measured for the sibling spelling and CLOUD-1051 for the declaration table, one shape further on.

CLOUD-1221 retired ready-lint.sh and did not hit this, which is worth stating because it bounds the row: board-sweep.sh reaches ready-lint transitively through graph-check, never by building its name. The loop at :229-230 is over the gates board-sweep invokes directly.

Two shapes could close it, and they are not equivalent

A token-removal arm. Admit an added line whose only difference from a removed one is that a token this delta retires has been dropped from a list — removing a name from it is the honest edit when that gate no longer exists. Narrow, and it never introduces a byte.

(Correction, while building this: the list is not a gates=(...) array, as the Why above paraphrases it. board-sweep.sh:229 names the gates inline in the loop header — for gate in graph-check duplicate-close-check released in-progress-drain done-pr-check spec-ref-check; do. The shape of the fix is unchanged; the fixture had to match the real line rather than the paraphrase.)

A loop normalisation. Change board-sweep.sh so each gate is invoked on its own line naming its own path — the shape hooks-wiring-check.sh was normalised into for exactly this reason (policy/shell-retirement.rego's own comment: "with each row on its own line every row is independently removable, and the next retirement in this campaign does not have to shorten anything at all"). That makes the file permanently retirement-friendly, but the normalisation is itself an edit to a governed program and has to land through an admission that does not yet exist — so it cannot bootstrap itself.

The first is the landable one and the second is what it unlocks. Decide on the row; do not take the second as a given.


Refinement — Ready (admit dropping a retired name from a list)

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

  • **Authority boundary (§1). **policy/shell-retirement.rego's admitted_addition gains a fourth disjunct; batten.toml gains any [[pattern]] row it needs. No governed program is edited by this row — board-sweep.sh is the consumer whose future edit this makes landable, and touching it here would need the very admission being added.
  • Computable predicate (§2). An added line is admitted when it equals a removed line with one whitespace-delimited token deleted, and that token names a path this delta deletes — matched through forms computed from gone rather than by string similarity. (Built: naming_forms yields the path, its basename, and its basename without the extension. The third is the one this needs, because the loop names TASKS and the files carry .sh CLOUD-865's split — so the token is graph-check, not any filename.)
  • Both halves had to move, which §1 did not anticipate. The addition arm alone is not enough: only_drops_a_retired_reference also requires every REMOVED line to mentions_retired, and for gate in old-gate other-gate names no path by any arm, so the removal half refused it independently. mentions_retired therefore gains the bare-name form too. That arm is looser than its siblings — a contains over a name — and it is safe for the reason the clause's own comment gives for the others: it decides a line that is GOING AWAY. What is introduced is checked by the addition arm, which requires byte equality with that line minus exactly that name. Neither half is tight; the pair is. The narrowings that make the third disjunct safe apply unchanged: the dropped span is DERIVED from the two lines rather than declared, and it must reference a deleted path, so an unrelated token cannot be smuggled out of a line.
  • Deliberately not in scope (§2). Normalising board-sweep.sh's loop (the second shape above). Admitting an added line that drops a token naming nothing retired — that is an ordinary edit and must stay refused. Widening to mise.toml or any ungoverned file, which needs no admission.
  • **Effect (§3). **read. A policy module decides over committed text.
  • Output & exit (§5). Unchanged: {rule, verdict, subjects} with a [[verdict]] row, pointer-only subjects, the crate's one 0/1/2/3 table. No new verdict token unless the refusal reason is genuinely new — reuse V-SHELL-RULE-EDITED, since this widens what that class admits rather than adding a class.
  • **Commit / bump (§6). **feat(policy)patch until 0.1.0. It adds a declarable shape rather than fixing a wrong verdict, which is why it is not fix.
  • Test obligation (§7). Both tiers, per .claude/rules/policy-modules.md: test_ rules in the module, and cases in crates/batten/tests/shell_retirement.rs over the compiled binary — the tier that proves the ENGINE builds the input, which is where both live instances of the dead-gate class were caught. The discriminator is the pair: board-sweep.sh:229-230's real shape must go from REFUSED to admitted, and a line dropping a token that names nothing this delta deletes must stay refused. The existing refusal case is rewritten rather than deleted, so the change of verdict is visible in the diff. Shown able to fail per CLOUD-418.
  • Mutation (§7). A declared #MUTANT that discriminates — not one another conjunct already excludes, which is the survival CLOUD-1149's own row measured. Note that shell-retirement is not in $MUTANT_GATES (mise.toml:457), so a declared mutation there is documentation until that is fixed; say so rather than claiming a red run.
  • Blockers (§8). None. relatedTo CLOUD-1149 and CLOUD-1219 (the two arms this extends, and the module this edits), CLOUD-1221 (the retirement that exercised them and found this gap still open), CLOUD-1051 (the truncation arm and the same class one shape down), CLOUD-1121 (the path substitution).

Acceptance

  • board-sweep.sh's gates=(...) line, with one retired name removed, is admitted; the same line with an unrelated token removed is refused.
  • The existing refusal case in crates/batten/tests/shell_retirement.rs is rewritten to the new verdict, not deleted.
  • Both tiers green; the declared mutation discriminates.
  • No governed program is edited by this change.

Found while landing CLOUD-1149/CLOUD-1219: the admission was widened twice and this caller still matched nothing, which was recorded in a PR body and nowhere the board could see.

CLOUD-1142 An issue key gets one definition, in the fact producer — the three axes decided, three-valued, and a gate that fails the twenty-first copy

Why

CLOUD-761 measured twenty independent derivations of the issue-key regex across nine spellings, diverged on three axes — case, boundary, and whether the project prefix is required at all — with a shipped defect behind them: a PR body writing closes cloud-757 is accepted by closing-key-check and then invisible to claimed-keys and released. Two gates disagree today about what an issue key is.

That row decided all three axes and then named the whole remedy as one piece of work: one definition, plus nineteen consumer conversions. The two halves have very different shapes, and only one of them is landable.

**Seventeen of the nineteen consumer sites live in governed **mise-tasks/ programsready-lint.sh, graph-check.sh, closing-key-check and released among them. Converting a consumer in place raises shell edit refused, which declares one route, rule read first, with no override and no bypass_env. So the conversion half stays on CLOUD-761 with that blocker named, and this row is the definition half, split out so it can ship.

Withdrawn, 2026-09-01: there is no "cannot retire" set. This paragraph asserted that three of the seventeen "cannot retire at all, being resolved by path from siblings that would themselves need an added line to be repointed." That is false in both of its halves. The reasoning cites only_drops_a_retired_reference, which is the REMOVAL clause, to conclude about ADDITIONS — those are governed by admitted_addition, whose arms already admit a path repointing at a declared successor and an invocation repointing at a declared command, which is exactly the added line the claim says is unpayable. One of the three named programs, hooks-wiring-check.sh, is no longer tracked on origin/main at all: it was already retired while the claim stood. The same withdrawal is recorded on CLOUD-761.

Nothing here is a new decision. Every axis this row builds, CLOUD-761 already decided, and its wording is carried rather than re-argued.

Refinement — Ready

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

  • Authority boundary (§1). One definition of an issue key, in the fact producer under crates/batten/src/, declared as a typed board fact. batten.toml declares the identity, applicability, scope and severity of the anti-duplication gate over it; the generated schemas are derived by mise run schema and byte-compared, never hand-written. **Nothing under **mise-tasks/ **and no **tests/**/*.bats is added, edited or deleted here — this row converts no consumer, which is exactly what makes it separable from the half that cannot land.
  • Computable predicate (§2). The fact answers the key question once, on the three axes CLOUD-761 decided. Case: sensitiveCLOUD-757 is a key and cloud-757 is not one, and a body writing the lowercase form is refused rather than normalised, because normalising up is what produced the shipped defect. Boundary: the explicit character-class form (^|[^0-9A-Za-z-])…([^0-9]|$), not \b, so the rule does not depend on how a given matcher treats -. Project prefix: mandatory — a glob cannot anchor, which is why [A-Z]*-[0-9]* accepts AB-1, Z-9 and A-1foo, and CLOUD-[0-9]* accepts CLOUD-1x. The fact is three-valued: an unreachable tracker resolves to "could not look", never to an empty set that reads as a coherent board. The anti-duplication half is a gate over the tree: a second, independent derivation of the key pattern is a finding, because without one the twenty-first copy reappears and nothing notices, which is how twenty arrived.
  • Effect (§3). read. Board state is bounded and cacheable, resolved at verify / config lint time through the existing boundary; no verb changes class and no per-checker spawn is added.
  • Output & exit (§5). Pointer-only: a refusal names the key and the site, never a body. The crate's one 0/1/2/3 table with no per-verb exception — a malformed key is a policy verdict at 2, an unreachable tracker is 3, and 1 is a usage error. The 2/3 split must not collapse: that collapse is the vacuous pass this row exists to close.
  • Commit / bump (§6). feat(facts)patch until 0.1.0, since below that release-plz bumps the patch whatever the type says. Not ! for the consumer surface: no exit code or output shape a consumer sees moves, and nothing that is accepted today by the sites this row actually touches stops being accepted, because it touches no existing site. mise run semver decides the library half, which is where a new fact type can move a pub surface without anyone meaning it to.
  • Test obligation (§7). crates/batten/tests/*.rs over the compiled binary, each case shown able to fail. The fixed example set is the discriminator and it is written down rather than left to the implementer: CLOUD-757 accepted; cloud-757, AB-1, Z-9, A-1foo and CLOUD-1x refused; and CLOUD-17 not matched inside CLOUD-179. The vacuity case, which a two-valued fact passes silently: an unreachable tracker must be distinguishable from a board with no relations. And the anti-duplication case, without which the whole row decays: a twenty-first independent derivation of the key pattern must fail the gate. Before that gate's severity is set to deny, replay the predicate over git rev-list origin/main and record commits examined, the would-fire count, and how many of those firings were false positives; the number is reported, never graded.
  • Blockers (§8). None on this half — the fact model's cost class and its three-valued contract, which CLOUD-761 waited on, have both shipped. relatedTo CLOUD-761 (the consumer half this was split from, which this row unblocks), CLOUD-757 (the fact model), CLOUD-251 (the vacuous-pass trap), CLOUD-773 (why an exit code cannot carry what the producer already knew) and CLOUD-418.

Acceptance

  • An issue key has exactly one definition in the tree, and a second derivation fails a gate rather than passing unnoticed.
  • All three axes are resolved by the stated behaviour rather than by whichever copy happened to run: case-sensitive, the explicit character-class boundary, project prefix mandatory.
  • The fact is three-valued — an unreachable tracker yields "could not look", never an empty set.
  • No governed shell program and no bats suite is touched, so the row lands without reaching the conversion half.

Not in scope

Converting the nineteen existing derivation sites, and issue-read-check:91's message-versus-glob disagreement along with them. That is CLOUD-761's remaining half and it is blocked on the shell retirement, not on this row. Also out of scope: the Ready-block clause structure beyond §8's relation, which CLOUD-761 already scopes honestly as prose that stays prose.

CLOUD-1231 The claim receipt holds ONE key per branch, but `closing-key-check` expects a PR to close several — so a multi-row branch can only ever record a claim for one of them

Why

Two gates disagree about how many rows a branch may serve.

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

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

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

Minting a claim for a second row on the same branch overwrites the first. There is no append, and the shape has no room for a second key.

Measured, 2026-08-31 on PR #770

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

Claiming CLOUD-1218 by hand would have erased CLOUD-1161's claim, so I did the board half (Todo → In Progress, assigned) and left the receipt alone. That is the right call for the tree and the wrong outcome for the record: the branch performed work for a row whose claim it cannot show.

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

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

Both are about the receipt being keyed by branch name — a stale claim inherited after a merge (516), and a rename stranding it (733). Both are now Done. This is a different axis: the key is fine, the arity is one where the workflow's own gate expects many.

CLOUD-502 asked a related board question (a bundle landed as one PR cannot be represented) and was cancelled. This row is narrower and mechanical rather than about WIP accounting: it is about what the receipt file can hold.


Refinement — Ready (let a branch's claim record every row it serves)

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

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

Acceptance

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

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

CLOUD-1311 The punt sweep is a NUDGE, so an agent reasons past it — CLOUD-514 called this ungateable, and the admission mechanism that landed since makes it gateable without judging

Why

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

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

It is a nudge. It has no exit code, and an agent reasons past it. Measured over one session, 2026-09-01: four deferrals, each with a principled-sounding blocker, every one of them false —

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

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

What actually held, and it is the whole design input

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

That is the signal. A punt does not survive because the reasoning is good; it survives because there is somewhere to put reasoning that costs nothing and is discarded when the turn ends.

Why CLOUD-514's exclusion no longer binds

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

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

True on 2026-08-13, and the reasoning is right: non-negotiable rule 3 forbids a gate resolving to a model verdict, and no predicate separates a punt from an honest split-out.

The premise is now obsolete, because the gate does not have to decide. CLOUD-1051 replaced override passwords with issued capabilities and CLOUD-1278 made the articulation durable by putting it in the commit message, hash-bound and verified by Articulation::recomputes. So the shape CLOUD-514 lacked exists: deny over an object, with the only exit an explanation written into history forever. The gate decides over the set; the author pays for the classification; nothing scores prose.

That is also what makes the false-positive worry dissolve. An honest split-out is never refused — it costs one admission naming the row and saying why it is independent work. CLOUD-514's own §2 already argues this direction: "the friction must sit only on the impulsive path." An articulation a reviewer will read is friction on exactly that path, and free on the other, because a row you genuinely could not close has an answer ready.

Refinement — Ready

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

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

Acceptance

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

Found when the repository's owner pointed out that the sweep is a nudge while the repository already has the mechanism for making an agent explain why a rule should not apply — and that the explanation gets written into durable history forever.

CLOUD-472 A §7 test obligation can land with no test behind it: the Ready block is checked for shape, the code for correctness, and the two are never compared

Why

ready-lint gates the shape of a Ready block. verify gates the code. Nothing compares them, so a §7 obligation can be written into an issue, pass the refinement gate, and land with no test behind it — and every gate in the loop stays green while it happens.

Measured on this repo's own work, 2026-08-12, on CLOUD-369. Its Ready block carried:

(b1) with a green holder and several waiters, exactly one waiter readies and pushes and the rest stay in draft, plus the negative — a holder whose CI is not green admits nobody

The first half shipped and is tested. The second half was never implemented and never tested. The landed land admits a successor the moment it loses the lease, with no reading of the holder's CI at all — so the acceptance bullet describing when a second matrix may be bought does not describe the code that buys it. #369 merged to main by fast-forward, CI-confirmed green, with that divergence in it.

The failure is structural, not inattention. Reconstructing it:

  1. The acceptance was written first, and correctly.
  2. A collision with CLOUD-420 forced a mid-implementation redesign of the mechanism.
  3. The redesign was derived from the problem again, not from the acceptance — and silently dropped one condition.
  4. The tests were written from the implementation, so they assert what the code does. A test for the dropped condition was never written, because the code it would test was never written.
  5. Nothing anywhere reads the §7 clause and asks whether a test names it.

Step 4 is the load-bearing one. Tests written from the implementation can only ever confirm it; they are structurally incapable of catching an obligation that was dropped, because a missing behaviour has no code to write a test against. The Ready block is the only artifact that still remembers what was promised, and nothing reads it at implementation time.


The four open questions are ANSWERED, 2026-09-01 — and the first answer was already in the tree

This row sat in Backlog because its four questions were unanswered design decisions. Answering them turned up what the questions did not anticipate: three of the four already have a landed mechanism. The reason they are not working is not that the mechanism is missing — it is that it is OPTIONAL.

1. The binding token — CLOUD-453's claims object already is one

A Ready block may be written in either of two dialects. The prose dialect is validated for the clauses that are present. The object dialect is a fenced json block, and REQUIRED_CLAIMS in crates/batten/src/ready.rs demands source_of_truth, gate, commit_type, blockers and tests — each present and non-empty. check_claimed_tests then demands file **and **mutation on every entry, which is CLOUD-418's obligation as a field: an entry that cannot name the mutation which would kill it cannot be written at all.

That is the third form question 1 asked for, and it landed. The module's own comment says why it works: a key cannot be well-formed prose.

**MEASURED, IT IS UNUSED. **check_claims returns false when there is no fence, and the caller falls back to the prose path. Sampled 2026-09-01 over the 50-row Todo queue: CLOUD-1306, filed the same day, carries no object, and its §7 reads "Over the compiled binary or the successor's own tier. Shown able to fail per CLOUD-418, three discriminating observations: …" — three obligations, none joinable to anything. CLOUD-1311's own Ready block, written the same day, is the same shape and linted green.

So the defect is not a missing mechanism. The escape is that the prose dialect stands as an ALTERNATIVE rather than as a legacy.

ready.rs already anticipates this and already emits the measurement: it pushes dialect json or dialect prose per run, with the comment "A prose-only block still PASSES — every issue Ready today stays Ready, which is what lets the corpus converge deliberately instead of in one sweep." The convergence was left to intent, and intent did not converge it. The emission is the sensor; what is missing is the ratchet.

2. Where it runs — committed, and the declared set reaches the tree through the recorder

Decided by a property measured while landing CLOUD-1302: the filed-here record store lives under $GIT_DIR, is never committed, and dies with the container. A pull-time-only gate cannot be re-asked, and this row's whole defect is one found after the merge.

tests[].file entries are paths in the tree, so half the join is already about it. The DECLARED set reaches the tree through a surface that exists and is forgery-resistant: board-issue-groomed's verdict column lints the description the tracker returned, never a payload the caller assembles — earned, since ready-lint over a self-assembled payload was measured green three times against text in a local file, once under an id no row carried.

3. Ready AND land — two predicates, not one placement

The body treated this as either/or. It is neither.

  • At refinement, ready-lint asserts the obligations are declared and well-formed. Nothing is implemented yet, so nothing about coverage is asserted and there is no false failure.
  • At verify, a tree gate asserts each declared file exists and each declared mutation is caught. Late, but re-askable forever.

4. Deferred-but-covered — the [rule.conserves] ledger already spells it

// carried:, // subsumed:, // changed:, // withdrawn:, // ported:, each resolving its target by exact tracked path, with CASE_TARGET_MISSING when the named successor dies. CLOUD-369 §7(c), satisfied by another PR, is a subsumed arm. An obligation with neither a bound case nor an arm is the refusal, and that is the whole predicate.

Subagents are a producer, never the judge

Non-negotiable rule 3 forbids a gate resolving to a model verdict, so "spawn a reviewer and trust its finding" is unavailable. The role that is not that: a subagent PROPOSES a candidate mutation for an obligation carrying a file and no discriminating mutation, and mise run mutant decides — apply the expression, run the named case, it must go red; a survivor is the finding. Measured value this session, twice on one file: a declared mutation on privileged-lane survived because another conjunct already excluded its input, and a shell-retirement row was malformed because a | inside a Rego comprehension parsed as the field separator. Neither was findable by reading.


Refinement — Ready (increment 1 of 2: close the escape, then bind the obligations)

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

  • **Authority boundary (§1). **crates/batten/src/ready.rs (the dialect decision in check_claims and its caller), the consumer-declared cutover in batten.toml, and a compiled-binary tier in crates/batten/tests/it/ready.rs. No change to the claims grammar, to REQUIRED_CLAIMS, or to check_claimed_tests — those already say the right thing and are simply unreachable.
  • Computable predicate (§2). A Ready block carrying no claims object is a finding when the row was created at or after a consumer-declared date. Before that date the prose dialect passes exactly as today. The comparison is the payload's own createdAt against a fixed-width ISO-8601 literal, so it resolves with no network call and no second read of the tracker.
  • A RATCHET, NOT A FLIP, AND THE MEASUREMENT IS WHY (§2). Flipping it unconditionally refuses the entire Todo queue in one step — 50 rows, and the sample carries no object at all — while graph-check enforces Todo ⇒ ready-lint exits 0, so the board's whole ready frontier would go dark at once. That is CLOUD-858's shape, where three rows made graph-check refuse the whole board. Grandfathering by creation date closes the escape going forward and costs the standing queue nothing.
  • Could-not-look is not a refusal (§2). A payload with no readable createdAt leaves the row judged exactly as today. A verdict about a missing field is not a verdict about the row, which is the three-valued posture the rest of this engine already takes.
  • The date is the CONSUMER's (§2). It is a fact about this repository's own board, so it lives in batten.toml and never in crates/batten — non-negotiable rule 1, the same reason the tracker's vocabulary is not the crate's to name.
  • Deliberately not in scope (§2). Binding a declared tests[].file to a case in the tree and holding a declared mutation to the sweep — that is increment 2, and it is the half this row was originally filed for. Changing what REQUIRED_CLAIMS demands. Migrating any existing row's body. Retiring the prose path outright.
  • **Effect (§3). **read.
  • Output and exit (§5). The one contract: 0 clean, 1 the finding, 2 could-not-look. Pointer-only — the block's line and the rule id, never a clause's text.
  • **Commit / bump (§6). **feat(ready) — patch (0.0.x).
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/it/ready.rs. Shown able to fail, four discriminating observations: a post-cutover row with no object is refused; a post-cutover row WITH an object is clean; the anti-vacuity mirror — a pre-cutover row with no object is clean, without which the arm is satisfied by a check that refuses every prose block; and an unreadable createdAt leaves the row judged as before.
  • Blockers (§8). None. relatedTo CLOUD-453 (which built the claims object this makes reachable), CLOUD-418 (the mutation field), CLOUD-1311 (the sibling escape — a nudge with no exit code, where this is a mechanism with no reachability), CLOUD-858 (the measured cost of refusing a whole board at once), CLOUD-369 (the instance).

Acceptance

  • A row filed after the cutover cannot reach the ready queue with a prose §7.
  • The 50 rows standing in Todo today are unaffected, and graph-check still passes over the board.
  • An unreadable creation date is a pass, not a refusal.
  • Nothing scores prose, and no clause is judged for what it says.

Filed from the losing side: the divergence above was found by re-reading the issue after the merge, which is not a mechanism. The instance was fixed under CLOUD-369; this is the gate that would have caught it.

CLOUD-761 Nineteen issue-key consumers still re-derive the key, and every one of them is a governed shell program the conversion cannot touch

Lifecycle bundle integration

Extend canonical board facts with status, lifecycle revision, semantic body digest, structured relations, claim generation, bundle roster, and PR/main/release evidence. An unreachable tracker must yield explicit unknown facts, never an empty clean payload.
The largest amortisation win in the tree, and the one with a shipped defect already attached.

Board state — issue status, relations, Ready-block clauses, issue keys — is re-extracted from markdown by every checker that needs it. Measured:

Signal Count
Independently re-derived copies of the issue-key regex 20 sites, 9 spellings — diverged on three axes, see below
ready-lint: markdown-dialect extraction → predicate ~95 lines → ~23 lines
claim-race-check: fetching → predicate ~50 lines → ~6 lines
graph-check: total → genuinely relational part ~700 lines → under 60

Those ratios have been read before as "an engine would only improve the small half". That reading is backwards: they are the price of nothing amortising extraction, paid once per checker, forever. Twenty derivations of one regex is not evidence that regexes are expensive — it is evidence that no fact exists.

And they have already diverged — on three axes, not one. Re-counted 2026-08-20 over code lines only (mise-tasks is 43–53% comment, so a naive grep inflates). The original "nine copies, diverged in case-sensitivity" undercounted the sites and missed the worst axis entirely.

Axis 1 — case. Five sites match case-insensitively and normalise up (closing-key-check:68, :98, claimed-keys:59, bot-issue:357, ready-lint:360); nine match case-sensitively (released:97, graph-check:216, :291, :302, ready-lint:314, bot-issue:295, :379, :436, deferral-check:104). Two expressions are mixed within two lines of themselves: bot-issue:357 matches insensitively and :358 re-extracts sensitively.

Axis 2 — boundary. (^|[^0-9A-Za-z-])…([^0-9]|$) (landed-check:63, done-check:108, both commented "so CLOUD-17 does not match CLOUD-179") versus \b…\b (board-move-guard:142, different semantics — - is a word boundary) versus grep -qxF (claim-race-check:127) versus unanchored.

Axis 3 — whether the project prefix is required at all. This was not in the original count, and it is the worst of the three. Four sites are shell case globs, not regexes: board-move-guard:104, issue-read-guard:71 and issue-read-check:88 all test [A-Z]*-[0-9]*; hooks-wiring-check:202 tests CLOUD-[0-9]*. Verified behaviour of [A-Z]*-[0-9]*:

input verdict
CLOUD-757 accept
AB-1 accept
Z-9 accept
A-1foo accept
cloud-757 reject

And issue-read-check:91 prints "is not an issue key (want CLOUD-123)" — an error message asserting a contract its own glob does not enforce. hooks-wiring-check:202 accepts CLOUD-1x for the same reason: a glob, not an anchored pattern.

The shipped defect is concrete. A PR body writing closes cloud-757 is accepted by closing-key-check and then invisible to claimed-keys and released. Two gates disagree today about what an issue key is. That is not a risk.

Cost class: read

Board state arrives over the tracker API — bounded, cacheable, resolvable at verify/config lint time. No 100ms budget question, and no §5 spawn question if resolved through the existing boundary rather than per-checker.

What this issue lands

  • One authority for an issue key, replacing twenty derivation sites. This alone justifies the issue.
  • Typed board facts: status, relations (blockedBy / blocks / relatedTo), milestone membership, and the Ready-block clause structure — as data, not as a markdown substring hunted per checker.
  • The extraction paid once. ready-lint's ~95 lines become a fact producer with one consumer today and several tomorrow; its ~23 lines of predicate stay predicates.

A caution this must respect. CLOUD-623's survey classified invariants by input shape and warned that where the hard part is turning prose into structure, an engine over already-normalized facts cannot help. That warning is right about engines and is exactly why this is a fact issue: the point is to do the extraction once and well, not to move the predicate to a different language. The Ready-block clause structure is the hard case — it is markdown with a dialect — and it should be scoped honestly: some clauses are structural (§8's blockedBy cross-check is already a relation), others are prose and stay prose.

A trap already recorded, not to be rebuilt. CLOUD-251"a rule set with no relations still reports the board coherent" — is the vacuous-pass failure. A board fact that resolves to "no relations" must be distinguishable from "could not look", which is CLOUD-757's three-valued contract doing real work here.

Acceptance

  • An issue key has exactly one definition; the other nineteen sites consume it. Asserted by a gate, or the twenty-first copy reappears.
  • All three divergences are resolved deliberately — one behaviour chosen and stated for case, for boundary, and for whether the project prefix is mandatory — not whichever copy happened to run. The four glob sites are the ones that change behaviour, so they need naming individually.
  • issue-read-check:91's message and its glob agree, in whichever direction is chosen.
  • Board facts are three-valued: an unreachable tracker yields "could not look", never an empty set that reads as coherent (CLOUD-251's trap).
  • At least one existing checker consumes the fact rather than re-extracting, with its extraction half deleted — the proof that amortisation is real rather than asserted.

Two findings filed 2026-08-20 point here. CLOUD-772 holds the volume half of the same defect: 73 independently hand-rolled parsers across nine committed artifacts, report() defined in 39 task files, exit 2 hand-rolled in 76, and zero source/. lines in the whole mise-tasks tree — there is no shared library and never has been. CLOUD-773 supplies the mechanism rather than another count: graph-check spawns ready-lint — the one program in the tree that parses a Ready block and identifies every key it cites — and then re-derives the issue-key regex itself at :216, :291 and :302, because an exit code cannot carry what the producer already knew. That is why the copies exist, and it is why one authority is necessary but not sufficient.

Filed from a subprocess-boundary audit; the ratio data is CLOUD-623's and CLOUD-647's, read the other way round.

Probe plan

  • Unresolved decision: canonical issue-key behavior for case, boundary, and mandatory project prefix.
  • Probe: run the named existing checkers over the recorded key examples, including closes cloud-757, AB-1, A-1foo, and CLOUD-1x, before and after routing one checker through the shared board fact.
  • Record: each checker verdict by example, selected canonical behavior, unreachable-tracker result, and evidence that one consumer no longer re-extracts the fact.
  • Ready when: all three behaviors are explicitly selected, the checker results agree, unreachable resolves to “could not look,” and one extraction half is deleted; then expand consumers from that authority.

Refinement — Ready (one definition of an issue key, and all three axes chosen rather than inherited)

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

The three divergences are decided here, because "resolve them deliberately" is not a predicate until the resolutions are named.

  • **Case: sensitive. **CLOUD-757 is a key and cloud-757 is not one, and a body writing the lowercase form is refused rather than normalised. Normalising up looks friendlier and is what produced the shipped defect: one gate accepts the spelling, two others cannot find it, and nothing says so. The cost is stated rather than hidden — a PR body writing closes cloud-757 fails closing-key-check where today it passes, which is the refusal that should always have been there.
  • **Boundary: the explicit character-class form **(^|[^0-9A-Za-z-])…([^0-9]|$), not \b. It states both sides in one place and does not depend on how a given matcher treats -, which is precisely what let two sites disagree while both looking correct.
  • Project prefix: mandatory. The four shell case globs are replaced outright, not tightened: a glob cannot anchor, which is why [A-Z]*-[0-9]* accepts AB-1, Z-9 and A-1foo, and why CLOUD-[0-9]* accepts CLOUD-1x. issue-read-check:91's message already asserts the prefix is required, so the message is right and the glob is what changes.
  • Authority boundary (§1). The definition itself is no longer this row's — it was split out so it could ship, and this row is the consumer half: the nineteen sites that re-derive the key each stop doing so and read the fact instead, with their extraction halves deleted. That is what makes §1 clean here and the row blocked all the same, because seventeen of those sites are governed mise-tasks/ programs. The Ready-block clause structure is scoped honestly rather than wholesale: §8's cross-check is already a relation and becomes a typed fact; the prose clauses stay prose, per CLOUD-623's warning that an engine over un-normalised prose cannot help.
  • Computable predicate (§2). Two, both decidable. First, every consumer's verdict over a fixed example set — CLOUD-757, cloud-757, AB-1, Z-9, A-1foo, CLOUD-1x, and CLOUD-17 inside CLOUD-179agrees, where today closing-key-check and claimed-keys disagree on the second. Second, board facts are three-valued: an unreachable tracker resolves to "could not look", never to an empty set. That is CLOUD-757's contract doing the work CLOUD-251 recorded as missing, and it is what stops a rule set with no relations reporting the board coherent.
  • **Effect (§3). **read. Board state is bounded and cacheable, resolved at verify/config lint time through the existing boundary rather than per-checker, so no verb changes class and no per-checker spawn is added.
  • Output & exit (§5). Unchanged and pointer-only: a refusal names the key and the site, never a body. Exit follows the one table — a malformed key is a policy verdict at 2, an unreachable tracker is 3, and the two must not be collapsed, since that collapse is the vacuous pass this row exists to close.
  • **Commit / bump (§6). **feat(facts)patch until 0.1.0, since below that release-plz bumps the patch whatever the type says. Not ! for the consumer surface: the change only ever adds refusals — cloud-757, AB-1, A-1foo and CLOUD-1x stop being accepted — which house style §8's raise-only invariant permits, and no exit code or output shape moves. mise run semver decides the library half, which is where a new fact type can move a pub surface without anyone meaning it to.
  • Test obligation (§7). Over the compiled binary and, for each converted consumer, whatever tier survives that consumer's own retirement — which is part of what is unresolved, since a consumer that retires takes its bats suite with it and a consumer that does not cannot gain a case. Each case shown able to fail per CLOUD-418. The discriminator is the shipped defect and it is red today: closes cloud-757 must produce one verdict across closing-key-check, claimed-keys and released, where today the first accepts it and the other two cannot see it. The four glob sites need naming individually because they are the ones whose behaviour changes: AB-1, Z-9, A-1foo and CLOUD-1x are accepted today and refused after. The vacuity case, which a two-valued fact passes silently: an unreachable tracker must be distinguishable from a board with no relations. And the anti-duplication case, without which this whole row decays: a twenty-first independent derivation of the key must fail the gate.
  • Blockers (§8). This row is blockedBy CLOUD-1142, which holds the definition half and must land first, and blockedBy CLOUD-757 for the fact model's cost class and its three-valued contract. relatedTo CLOUD-251 (the vacuous-pass trap), CLOUD-772 and CLOUD-773 (the volume half and the mechanism half of the same defect), CLOUD-623 (the caution that keeps this a fact row rather than an engine row) and CLOUD-418.

Split note, 2026-08-29 — the definition landed elsewhere; what is left here is blocked, and by what.

This row asked for one definition of an issue key and the conversion of nineteen consumers. The first half is landable and the second is not, so they are now two rows and this one keeps the harder half.

CLOUD-1142 carries the definition: the fact producer under crates/batten/src/, the three axes exactly as decided above, the three-valued could-not-look contract, and the gate that fails a twenty-first derivation. It touches no governed path and can be built today.

THE "CANNOT RETIRE" CLAIM IS WITHDRAWN (2026-09-01) — it was false in both halves, and it has already been paid for

The paragraph below said three programs cannot retire at all. Withdrawn rather than softened, because it is disproven twice over and it has already cost a landed retirement.

**Disproven by demonstration. **hooks-wiring-check.sh is one of the three. It is no longer tracked on origin/main. It retired.

Disproven by reading the module. The reasoning cites only_drops_a_retired_reference — the REMOVAL clause — and concludes about ADDITIONS. Additions are governed by admitted_addition, which has four arms; one of them, repoints_at_the_declared_successor, is an ADDED line swapping a retired path for the successor its own ledger declares. Repointing a sibling is exactly that arm's case. CLOUD-1299 has since added a fifth, admitting a caller that names its callee as a TASK rather than a path.

Already paid for. Replaying the key-derivation count over 400 commits of origin/main fires exactly once: c64e54a3, "take back the Ready-grammar retirement, whose successor cannot spell a sibling" — a revert that put four derivations back, on this same false premise.

The lesson generalises past this row: a blocker asserted from one rule while the admitting rule sits beside it is a block reported as a decision, which AGENTS.md already calls a bug. No row should cite "cannot retire" again without naming which admitted_addition arm fails and why.


What stays here is the conversion, and it is blocked on the retirement campaign's ordinary sequencing rather than on any impossibility. Seventeen of the nineteen sites are mise-tasks/ programs — ready-lint.sh, graph-check.sh, closing-key-check, released, landed-check, done-check, bot-issue, board-move-guard, issue-read-guard, issue-read-check, hooks-wiring-check, deferral-check, claim-race-check and claimed-keys among them. Editing one in place raises V-SHELL-RULE-EDITED, which declares one route, R-PORT-AND-RETIRE, with no override and no bypass_env. Retiring them instead is the sanctioned route, and it is available for all of them. ready-lint.sh is resolved by path from graph-check.sh:163 and pipefail-grep-check.sh, and graph-check.sh from board-sweep.sh and released.sh — sibling constructions, which is_retired_reference's arm 2 recognises and repoints_at_the_declared_successor admits repointing. hooks-wiring-check.sh carried a DECLARED table, and drops_a_retired_name (CLOUD-1224) exists for precisely a list that names a program by stem; it has since retired, which settles that one by demonstration rather than by argument.

So this row's acceptance clause — "at least one existing checker consumes the fact rather than re-extracting, with its extraction half deleted" — arrives with whichever consumer retirement lands first, and it belongs to the shell-retirement campaign's sequencing rather than to this row's own effort. It is recorded here so the next reader does not rediscover it as a surprise mid-build.

The definition half has landed (CLOUD-1142): ready::Grammar::key_of and keys_in own the three axes, the token stays the consumer's in [[pattern]] ready-issue-key, and issue-key-derivations-not-growing ratchets the task tree so the count cannot grow while these conversions wait. Replayed over 400 commits before its severity was set: 400 examined, 1 would-fire, 0 false positives.

CLOUD-841 `config-lint` reads a receipt that names no weakening as "no receipt", so a commit trailer alone admits a weakening the groom never saw

Why

CLOUD-789 gave config-lint its groomed-weakening hatch, and the design is stated in that file's own header:

  • The GROOMED BODY names it — a **Weakens:** … clause … claim-check copies it into the branch's claim receipt as a weakens line, at the one moment a gate holds the groomed body and the work has not started.
  • A COMMIT names it — a Weakens: <smell-id> <key> trailer …
  • They AGREE. Where both are readable, a trailer naming something the groom did not is refused, and the trailer alone admits nothing.

"The trailer alone admits nothing" is not what the code does. mise-tasks/config-lint reads the groom as:

groomed=$(grep -E '^weakens ' "$claim" || true)

so a receipt that exists and names no weakening yields the same empty string as no receipt at all. The admission arm then keys on that:

if [ -n "$trailers" ] && grep -qFx "$smell $key" <<<"$trailers"; then
    if [ -z "$groomed" ]; then
        echo "config-lint: admitted $smell $key (commit trailer; no claim receipt here to check it against)"

The message says "no claim receipt here" — CI's honest state, where none was ever written. On a developer branch the receipt is right there under $GIT_DIR and says, readably, this work was groomed and no weakening was admitted. That is not absence of evidence; it is evidence of absence, and it is read as the former.

Measured 2026-08-21 on claude/ratchet-retires-with-subjects-be87wn (CLOUD-807's branch). The receipt existed and named no weakening:

CLOUD-807
ready-lint pass
takeover 1 refusal(s) overridden (BATTEN_CLAIM_TAKEOVER): CLOUD-807 assigned
claimed-at 2026-08-21T07:50:43Z
...

Adding a Weakens: trailer to a commit, with the Ready block edited after the claim, produced:

batten.toml:rule[bats-tests-not-deleted].retires_with rule-predicate-changed
config-lint: 1 smell(s)
config-lint: admitted rule-predicate-changed rule[bats-tests-not-deleted].retires_with (commit trailer; no claim receipt here to check it against)
config-lint: 1 smell(s), every one admitted by a groomed decision

The last line is the defect in one sentence: it says groomed decision about a decision the groom never saw. The weakening in that instance was real and deliberate and is recorded on CLOUD-807 — the point here is that nothing forced it to be, and the local half that the header calls "the strong half" did not run.

Why this matters more than a wording fix. CLOUD-789's whole argument is that the strong check runs where the receipt is — verify, before a CI runner is spent — and CI confirms the committed half. As written, the strong half is unreachable on any branch whose groom recorded nothing, which is every branch that did not anticipate its own weakening. That is exactly the population the gate exists to catch: an author who knew would have groomed it.

The three states are two in the code. absent (no receipt — CI, or a branch with no claim), groomed-nothing (a receipt naming no weakening), and groomed-this (a receipt naming the pair) must be distinguished; today the first two are one. Only absent may fall back to the trailer.

Note claim-check must keep minting a receipt when the groom named nothing — it already does — so the distinguishing fact is receipt-exists, not weakens-line-exists.

Prior art on the same shape. This is CLOUD-615's class exactly: a fallback that reads "could not look" for "looked and found nothing", where the lenient branch is the one that fires. CLOUD-820 is removing the same shape from claim-check's own clock fallback, and its Ready block's reasoning ("delete the clock fallback rather than leaving it as dead code") applies here unchanged.


Refinement — Ready (three states, and only absence falls back)

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

  • Source of truth (§1). The claim receipt's existence is what separates "could not look" from "looked and found nothing"; its weakens lines are what separate the third state. No second record of either.
  • Computable predicate (§2). For each pointer batten.toml:<key> <smell>: admit iff a Weakens: <smell> <key> trailer exists in origin/main..HEAD and either (a) the claim receipt for this branch is unreadable/absent, or (b) it carries a matching weakens line. A readable receipt with no matching line refuses, whatever the trailer says. Decidable from the filesystem and git log, no network.
  • Effect (§3). read. No new verb, no new column.
  • Generated artifacts (§4). None.
  • Output & exit (§5). Pointer-only, unchanged: batten.toml:<key> <smell>. The refusal must say which of the two reasons applies — "no groom recorded for this branch" is a different remedy from "the groom named a different pair" — and must not quote the Ready block.
  • Commit / bump (§6). fix(gate) — a shell task, no crate API touched.
  • Test obligation (§7). tests/config-lint.bats. The discriminating case is the one that is green today and must go red: a receipt that exists, names no weakening, plus a matching commit trailer → refuses. Plus the three that must stay green: no receipt at all + trailer → admits (CI's path); receipt naming the pair + trailer → admits; receipt naming a different pair + trailer → refuses (already covered). A #MUTANT row reverting the arm to [ -z "$groomed" ] must turn the first case green again, or the fix is untested.
  • Blockers (§8). None. relatedTo CLOUD-789 (which built the hatch), CLOUD-807 (where this was measured), CLOUD-615 and CLOUD-820 (the same could-not-look-versus-found-nothing shape).

Acceptance

  • A branch whose receipt names no weakening cannot admit one by commit trailer.
  • CI, where no receipt exists, is unaffected — the committed half still confirms what the local half proved.
  • The admitting message no longer says "no claim receipt here" when a receipt is present and readable.

CLOUD-516 The claim receipt is keyed by branch name, so a branch restarted after its PR merged inherits a stale claim and `claim-guard` passes on it silently

Why

Measured 2026-08-13. .git/batten-receipts/claim.claude-groom-cloud-491-qv83e7 contains CLOUD-230. The work done on that branch this session was CLOUD-507, CLOUD-505, CLOUD-456 and CLOUD-513 — none of which CLOUD-230 has anything to do with. claim-guard was satisfied on every edit behind all four, by a claim for an unrelated issue, and reported nothing.

The cause is that the receipt is keyed by branch name, and a name is not a branch. After its PR merged, the branch was restarted twice with git checkout -B claude/groom-cloud-491-qv83e7 origin/main — the documented remedy for a merged PR, since new work must not stack on already-merged history. That repoints the name at a new base and discards the old commits. The receipt, living in .git/batten-receipts/ under the name, survived both.

Branch-keying was the right call and remains right for the case it was chosen for. claim-check's own header argues it: a claim attests to a decision about an issue that every commit on the branch continues to serve, so a SHA-keyed receipt would demand a re-claim per commit. That reasoning is sound. What it does not cover is the name outliving the branch it described.

This is the failure class this repo treats as worse than no gate. claim-guard did not error, did not warn, and did not skip — it passed, on evidence that had expired. A gate reporting green on state it never checked is the silent false green that linear-check's unguarded fetch, lock-check's regenerate-and-diff, and verify's unguarded body were each fixed for. The fix in every one of those cases was the same shape: record what the verdict was made against, and treat it as absent when that moves. linear-check's receipt already records the origin/main it was linear against; ready-guard's key to a SHA. The claim receipt records neither.

Refinement — Ready

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

  • Source of truth (§1). The claim receipt under $(git rev-parse --git-dir)/batten-receipts/, and git. No tracker read: the question is entirely about local state, which is what makes it answerable in a hook at all.

  • The naive predicate is wrong, and the reason is the landing loop (§2). The obvious rule — void the receipt when the branch's merge-base with origin/main moves — fires on every lap, because land rebases onto the current origin/main each time round and that is the loop working, not a fault. A rule that demanded a re-claim per lap would be refused within a day, and it would be refused correctly.

  • Mechanism as a computable predicate (§2). The receipt records the origin/main SHA it was minted against. It is void when both: the current merge-base with origin/main differs from the recorded one, and the branch carries no commits of its own (git rev-list --count origin/main..HEAD is 0).

    That conjunction is exactly a restart and nothing else:

    situation                                      base moved   own commits   verdict
    ---------------------------------------------------------------------------------
    claim, then work                               no           0 -> n        valid
    a lap rebases onto newer main                  yes          >=1           VALID  (the loop working)
    main moves, branch untouched                   yes          >=1           valid
    checkout -B <name> origin/main after a merge   yes          0             VOID
    fresh branch, fresh claim                      n/a          0             re-minted anyway
    

    A restarted branch is the one state that has both a moved base and nothing of its own, because the restart discarded the commits that were the branch. No timestamps, no reflog, no heuristics.

  • Effect (§3). read for the predicate; the receipt write already exists on claim-check's pullable path and does not change class. No new verb and no SURFACE change.

  • Output & exit contract (§5). claim-check keeps 0 pullable / 1 not pullable / 2 unreadable stdin. A void receipt reads as absent, so claim-guard's existing refusal and message are reused unchanged — the remedy is already "run claim-check for the issue you mean to pull", which is exactly right here. Pointer-only: the branch and the recorded base, never a body.

  • Commit / bump (§6). fix(claim-check) — patch until 0.1.0 regardless of type.

  • Test obligation (§7). tests/claim-check.bats and tests/claim-guard.bats, over a fixture clone, one row per line of the table above. Mutation-checked per CLOUD-418: with the own-commits half of the conjunction removed, the rebase row must go red — that is the direction a careless fix breaks, and the row that proves the conjunction is doing work rather than decorating.

    The regression case is this branch: a receipt naming an issue, then checkout -B <name> origin/main, then an edit — refused, where today it passes.

  • Blockers (§8). None. relatedTo CLOUD-431 — it widens the same receipt for a different question (was the block refined, and by whom), so the two touch one file and should be sequenced rather than merged; CLOUD-444 — it proposes retiring claim-guard into the engine while keeping branch-keying, and would carry this defect across unchanged; CLOUD-377 — the other way this receipt is not minted when it should be; CLOUD-514 — which proposes a third branch-keyed receipt and inherits this until it lands.

Acceptance

  • A branch restarted onto a new base carries no usable claim, and the first edit on it is refused.
  • A branch landing normally is never asked to re-claim, on any lap, however many times it rebases.
  • The rebase row is shown able to fail when the conjunction is weakened.
  • A receipt with no recorded base reads as void rather than as valid, so receipts predating this change do not grandfather themselves in.

Not in this issue

Whether the receipt should carry more than a base — the ready-lint verdict and updatedAt that CLOUD-431 adds for a different question. And retiring claim-guard into the engine, which is CLOUD-444's.

CLOUD-1051 Replace prose-only and filed-here override passwords with issued capabilities

The issue remains blocked by CLOUD-1050 and in Backlog until its typed precondition and Rego verdict ABI land.

Shell-retirement specialization

Generated Batten completions are derived output and are not authored shell rules. This issue's retirement boundary is every authored mise-tasks/** guard and Bats suite required by the two adapter migrations.

mise-tasks/prose-only-check.sh, tests/prose-only-check.bats, mise-tasks/filed-here-check.sh, tests/filed-here-check.bats, mise-tasks/land.sh, tests/land.bats, mise-tasks/stop-guard.sh, tests/stop-guard.bats, mise-tasks/closing-key-check.sh, and tests/closing-key-check.bats are not edited in place. Their predicates and callers are ported into Rego/Rust policy-engine surfaces, then the authored shell rules and corresponding Bats suites are deleted with carried, subsumed, or changed successor records and compiled-binary replacement coverage. Related shell utilities are either left byte-identical and outside the changed-file set or receive the same full retirement treatment; no new shell task, Bats suite, or fallback parser is allowed.

CLOUD-1059 enforces this changed-file rule. This issue is blocked by both 1050 and 1059.
Blocker. This is blocked by CLOUD-1050, which supplies the typed verdict and override precondition. It does not begin until that registry lands.

The capability protocol

This section is the single authority for the scheme; nothing below restates it.

The capability is a content-addressed admission, not a derived nonce and not a random bearer token. Both earlier forms were wrong in opposite directions: derivation from public tuple data alone is forgeable, and a random token leaves the record unauditable.

admission = H("batten-admission-v1" ‖ JCS({rule, verdict, subject, head,
                                          epoch, answers, prev, author}))

The serialization is canonical, not concatenation. Raw a ‖ b is ambiguous across field boundaries, so two different field splits can hash alike. One canonical form — JCS over a fixed object, or length-prefixed fields; the implementation picks one and the choice is part of what v1 pins — is what makes the address well-defined at all. epoch binds a repository/config generation, so an admission does not survive the policy change that would have made it unnecessary.

Authorization is the RECORD's existence and state, never possession of the name. This is the rule that must travel with the scheme: the hash's property is BINDING, not unguessability. Anyone holding the answers can compute it, so the address is neither a secret nor evidence of who created it.

The authority is the STORE; the address is integrity. Stated as a boundary rather than left implied, because an earlier wording here ("nobody but the issuer can create the record") claimed issuer authentication from a hash, which content addressing does not provide. What the address proves is that the record is internally consistent with its own fields. What restricts who may create a record is the store's write path, and nothing else. Under this row that write path is a local filesystem store, so the boundary is: anyone who can write the store can mint an admission. That is acceptable here — the threat model is honest error, not a hostile local writer — and it is written down so a later reader does not mistake the hash for a signature. If the trust boundary ever widens to a shared or remote store, the scheme needs a MAC or signature over the record; that is a separate row, never an inference from this one.

Because it authorizes nothing on its own, an admission is safe to print, log, quote in a commit and leave in a transcript — which removes the "never print a bearer capability in ordinary output" constraint that a log or PR body would eventually violate anyway.

What content addressing buys that a random token cannot:

  • The store becomes self-verifying. Recomputing the address from the record's own fields must equal the address, so editing the reasoning after the fact invalidates it. Under a random token, post-hoc tampering is undetectable — and the corpus is the entire diagnostic point.
  • Idempotence. The same reasoning yields the same address, so a repeated request is one record rather than one per attempt.
  • The ordinal disappears. Re-articulation stops needing a counter: reusing the previous answers reproduces the previous address, which is already spent, so overriding the same situation again requires genuinely different text. "Even in the same session" becomes a property of the construction.
  • prev makes the per-rule, per-subject history tamper-evident — no backdating and no reordering — which is a far stronger sensor than a flat append.
  • One value is capability, record key and audit reference, so the token and its record cannot drift apart.

The canonical in-toto receipt envelope stores the binding (rule, verdict, canonical-subject, HEAD, epoch), the answers, prev, author, and the spent state, keyed by the admission. It lives in the canonical out-of-tree receipt store at receipts/overrides/<fingerprint>.json; $GIT_DIR/batten-receipts/ is read for compatibility only during the two adapters' migration and is never an authority.

The store has no locking today, and this row must supply it. receipt.rs contains no locking at all — fs4 appears only in capture.rs — so the atomic issued → spent transition is a thing to build, not a thing to reuse. An earlier draft of this row asserted "under the receipt store's existing lock"; that was false, and the review caught it. What the row specifies instead:

  • Transition. Consume is a compare-and-set on the record's state, taken under an fs4 advisory lock over the override store — the same choice, for the same reason, that .claude/rules/rust.md records for the capture lock: the kernel releases it when the holder dies, so a SIGKILL mid-write leaves a reader a defined prefix rather than a lock nobody can release.
  • Chain head. prev is the previous admission for the same (rule, subject); the head pointer moves inside the same locked section as the state transition, so a chain cannot fork.
  • Concurrent consume. Exactly one consumer wins the compare-and-set; every other is a policy refusal (2), never an internal error.
  • Crash recovery. A crash before the CAS leaves the record issued and still consumable; a crash after leaves it spent. There is no intermediate state, because the state lives in one record replaced by one atomic rename.

Canonical subject, per gate, since the binding claim is untestable without one. prose-only-check's subject is the ordered set of tracked paths in origin/main...HEAD — the same set its refusal names — canonicalized as sorted repo-relative paths; the branch name is deliberately NOT part of it, so a rename cannot mint a fresh override for the same diff. filed-here-check's subject is the single CLOUD-<n> key of the row being overridden — one admission per row rather than one per invocation, so overriding three rows costs three articulations.

The two adapters are explicitly write-effect at consumption because they spend state. Their legacy variables remain supported only behind a named compatibility adapter during migration; the adapter emits a migration finding and is deleted in the same release once both Bats suites prove issued-capability flow. No other override family is claimed.

Why

BATTEN_PROSE_ONLY_OVERRIDE=1, BATTEN_FILED_HERE_OVERLAP=1, BATTEN_CLAIM_TAKEOVER=1, BATTEN_HOOK_BYPASS=1 and the rest share one property: they are knowledge. Grep the source — or read the refusal, which prints the name — and you hold the bypass forever, for every subject, in every session. Nothing is issued, nothing is scoped, nothing is spent.

That is why an override costs nothing to reach for. Measured 2026-08-25: an agent hit prose-only-check, held the env var name the moment it read the refusal, and put the override to a human. The change needed no override at all — the correct route was a clause the caller's paraphrase had dropped (CLOUD-1050 defect B). The override was the cheapest concrete thing in reach, so it was reached for.

The recording half already exists and is post-hoc

Two gates already write a record: prose-only-check appends to $GIT_DIR/batten-receipts/prose-only-overrides.<branch> and filed-here-check's BATTEN_FILED_HERE_OVERLAP "records which rows it overrode". Both write after the decision, as a trace.

CLOUD-786 is the row for the other half"the claim receipt records that a gate was overridden and no mechanism reads it, so a bypassed claim is indistinguishable from a clean one." This row does not duplicate it and does not block on it: 786 is nothing reads the record, this is the record is a precondition of release. They compose, and 786's deliverable is this row's sensor clause. Stated rather than absorbed.

What changes

The override stops being reachable by knowledge and becomes reachable only through a verb that issues it.

  • The gate never adjudicates the reason. Non-negotiable rule 3 — "a gate resolves to a command and an exit code over an object it decides, never a model verdict" — and "is this justification good" is exactly a model verdict. The predicate is reason present and well-formed ⇒ release. The forcing function is articulation, not approval. This is the clause that keeps the mechanism honest, and it is also why it works: the cost is thinking, not asking.
  • The admission is content-addressed over the proof chain (see the protocol section above). Single-use per SITUATION rather than per session, unharvestable onto a different subject because the binding is inside the hash, and re-articulation is forced by construction rather than by a counter — identical answers reproduce an already-spent address.
  • The questions must be falsifiable to the writer, or a fluent agent performs the ritual and proceeds. The three that would have stopped the measured instance, and the answers it would have had to type:
    1. state the precondition this gate declares and the fact satisfying itprose-only-check declares "the prose is the deliverable and cannot wait"; the diff was executable data, not prose, and it waited an hour before riding another branch. Both halves false.
    2. name what is lost if you do not override — nothing.
    3. name the route you rejected and why it does not apply — forces engagement with routes the reader may never have received.
  • Re-present the declined routes at issue time. Last cheap moment, and it catches the reader who never got route 1.

The corpus is a sensor on the GATE, not only on the agent

Twenty overrides of one rule is not twenty justified exceptions — it is a wrong predicate or an unreachable remedy. A per-rule override rate crossing a threshold should surface as a finding against that rule, which turns the log from an accountability record into gate telemetry. That is the more valuable artifact, and it is CLOUD-786's deliverable pointed at the gate rather than at the claim.

Composes with CLOUD-1050

The boundary that made this claim reach across scopes is closed. CLOUD-1050's registry was drafted policy-only, while both gates this row migrates are mise-tasks/*.sh — so consuming override.precondition from it was a claim across a boundary that row declared. CLOUD-1050 has since been broadened to cover refusal classes from both emitters (its Regal enforcement stays policy-only, because that is where a Rego AST exists to lint), so the dependency below is satisfiable as written.

That row makes override: {env, precondition} a typed field checked at authoring time — a gate declaring no precondition simply cannot be overridden, which is the right default. This row is that field's runtime consumer: the questions are generated from the gate's own declared precondition. One field, checked statically and spent at break-glass. If CLOUD-1050 lands first the env name disappears entirely; if it does not, the ritual still works off whatever precondition the gate states in prose.


Refinement — Ready

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

  • Source of truth (§1). One issuing verb plus the override record in the canonical out-of-tree receipt store (receipts/overrides/<fingerprint>.json), with $GIT_DIR/batten-receipts/ read for compatibility only during migration. The two gates that already write records route through it rather than keeping their own append — a second writer is the drift CLOUD-1050 is about, one layer down.
  • Computable predicate (§2). A gate's bypass is honoured only when a record exists at the presented admission, is unspent, and binds to the presented (rule, verdict, subject, HEAD); a bare env var is refused. Requesting one without answering every declared question is refused. Consumption is one atomic unspent → spent compare-and-set under an fs4 advisory lock this row introduces — the store carries none today. The address must recompute from the record's own fields, and prev must resolve, terminate and not cycle — the same chain predicate CLOUD-1053 needs for token successors, so one implementation serves both. All are lookups and comparisons — no judgement, no clock, no network.
  • Effect (§3). write, to the canonical out-of-tree receipt store named in §1. No new spawn, no network, no tracker.
  • Output & exit (§5). Pointer-only on the gate side — rule, verdict, subject pointer, admission — never the reasoning, which lives in the record. The admission is safe on this channel precisely because it authorizes nothing on its own. Exits follow the one table. Note the deliberate inversion: rule 4 keeps content out of output everywhere else, and here the reasoning IS the payload and belongs in the record, because it is the author's own words rather than repository content.
  • Commit / bump (§6). feat(receipt)! — a bare env var ceasing to work is a break in the consumer-visible surface and is declared, not hidden.
  • Test obligation (§7). Shown able to fail (CLOUD-418), each reddening one predicate: a bare env var with no admission ⇒ refused; an admission bound to a different subject or HEAD ⇒ refused; a request with an unanswered question ⇒ nothing issued; a spent admission ⇒ refused, while the same situation with genuinely different answers ⇒ a new address, issued; a record whose answers were edited after issuance ⇒ address no longer recomputes ⇒ refused; a prev chain that cycles ⇒ refused. Plus the anti-vacuity arm — a correctly requested and answered override succeeds, so a mechanism that refused everything would fail.
  • Blockers (§8). blockedBy CLOUD-1050, which supplies the typed verdict and the override.precondition the questions are generated from. relatedTo CLOUD-786 (nothing reads the record — this row's sensor clause is that row's deliverable; they compose rather than block), CLOUD-1050 (the typed precondition this consumes), CLOUD-680 (the menu-shaped ask this removes the pressure toward), CLOUD-820 (the prior instance of a bypass that was opt-out by deleting your own receipt).

Acceptance

  • No gate honours a bare env var.
  • An admission is unusable against a different subject, a different HEAD, a different config epoch, or a second time.
  • Two concurrent consumes of one admission resolve to exactly one success and one policy refusal.
  • An unanswered question yields no admission.
  • A record edited after issuance no longer recomputes to its own address, and is refused.
  • The record carries the answers, and a per-rule rate is derivable from it.
  • A legitimate override still completes, demonstrated end to end.

What this row must NOT claim

  • Not that it stops a determined agent lying. It makes lying legible across a corpus; it does not prevent one well-written false answer. The value is the aggregate and the friction, not detection.
  • Not that the gate judges. The moment the predicate grades a reason it becomes a model verdict inside a gate, which rule 3 forbids and which would be worse than today's password.
  • It adds latency to a genuine break-glass. The ritual must be one structured answer, not an essay. And if the issuer is unreachable the override is unavailable — fail-closed on the bypass, which is probably right but must be a decision rather than a discovery.

Grooming decisions — Ready

Bounded migration. This change migrates prose-only-check and filed-here-check only. Their existing override records are the evidence and their Bats suites are the adapter tests. Other BATTEN_* variables are not silently redefined; each requires its own migration row.

Issuer and store. Add batten override request as a write-effect CLI command. It takes --rule, --verdict, --subject, and structured answers generated from the rule's declared override.precondition in CLOUD-1050. It appends an override record under the canonical out-of-tree receipt store at receipts/overrides/<fingerprint>.json; $GIT_DIR/batten-receipts/ is read only for compatibility during the two adapters' migration.

Capability protocol. The issuer writes the record and returns its content address — see the protocol section above, which is the single authority for the scheme. The two migrated gates accept only BATTEN_OVERRIDE_ADMISSION; their legacy bare environment variables are refused. A verifier resolves the record, checks its binding and unspent state, and recomputes the address from the record's own fields, without a network call or a clock read.

CLI artifacts and contract. Add the command in crates/batten/src/cli.rs, surface.rs, and lib.rs; regenerate completions/* and man/* with mise run completions and mise run man, gated by mise run derived-check. Issuance and record writes are write-effect; gate output is pointer-only. Success is 0; malformed request or missing required answers is 1; a rejected or replayed capability is 2; internal store failure is 3. Land as feat(receipt)! → patch until 0.1.0.

Tests and rollout. End-to-end binary tests cover issuance, a valid use, a bare legacy variable, a different subject or HEAD, a spent admission, the same situation re-answered differently, a record tampered with after issuance, a cycling prev chain, two concurrent consumes of one admission (exactly one wins), a crash between issuance and consumption, and an unavailable issuer. tests/prose-only-check.bats and tests/filed-here-check.bats prove both adapters honor only the issued capability. The record must permit a per-rule override-rate report; that report is telemetry, never a judgement of the written reason.

CLOUD-514 Nothing prices filing over fixing, so spinning off a defect in the PR's own diff is arithmetically cheaper than finishing it

Why

Every gate in this repo prices failing to record something. finding-sink-check fails a turn that cites path:line evidence and makes no durable write. deferral-check fails a PR that defers a decision without naming an issue. stop-guard kicks a hedged flag. issue-guard refuses a PR that names no issue at all.

Nothing anywhere prices the opposite: recording something instead of doing it. Filing satisfies every one of those gates at once and costs a few seconds, while finishing costs a diff, a suite and a landing. For an agent under pressure that is not a temptation, it is arithmetic — and the board becomes the escape hatch every guardrail points at. AGENTS.md already names the behaviour: "A punt is any deferral you could have closed … offering an action you are already authorized to take." That rule is prose, and prose is feedforward only.

Nor is the substitution a fair trade. Across studies of admitted technical debt only 26.3–63.5% of it is ever removed, with median lifespans of 18–172 days and instances surviving more than ten years; in trackers specifically the repayment distribution is severely skewed, median 25 hours against a mean of 872 hours. A ~35× median/mean gap is the signature of a long tail never repaid at all. Filing does not defer a fix, it converts one into a weighted coin-flip.

Measured 2026-08-13, PR #390. CLOUD-513 is a defect in code written in that PR: two new fixture suites read ambient git config, passed verify locally and failed CI. The two suites were repaired in 4259045. The gate — a one-line [tasks."test:bats".env] declaration, in a file the same PR was already editing, whose cost had already been measured at zero (1475/1475 with and without ambient config) — was filed instead of applied. The PR merged green and the board gained a row a one-line edit would have made unnecessary.

No reviewer is present at the moment of the choice, so the cost has to land on the author. Landing here is trunk-based: a branch fast-forwards onto main and review happens after the merge, which is why unreviewed paths stay behind feature flags rather than behind a withheld merge. A mechanism that works by surfacing the punt to a reviewer therefore acts hours or days after the row was opened and the branch was closed. Whatever prices this has to be paid by the author, at the instant of filing, or it is not a price.

Two mechanisms are ruled out before any is proposed

1. Judging the spin-off is forbidden. "Is this issue related enough to the PR to belong in it?" and "should this have been fixed instead?" are both model verdicts, which non-negotiable 3 refuses: a gate resolves to a command and an exit code over an object it decides. CLOUD-505 hit the identical wall, and its resolution is the template — do not judge the content, price the action.

2. A time window is measured, and rejected. The obvious credential-free proxy for "issues this branch filed" is "issues created between the PR opening and its merge". Measured over the last 40 merged PRs against every CLOUD-* created in each window:

merged PRs read:            40
issues created in-window:   184
of those, absent from body: 183   (99.5%)

It fires on essentially everything, and structurally rather than tunably: this repo runs a fleet, so a window captures every session's filings. PR #373's window alone holds 20 issues, none of them its author's. A gate at 99.5% is switched off the first day.

That measurement is what makes a receipt load-bearing rather than convenient. Which branch filed which row has never been recorded anywhere — not in git, not on the PR, not on the board. Without a record the predicate is not merely noisy, it is not computable at all.

Refinement — Ready

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

  • Source of truth (§1): one record, minted from the tracker's own bytes. .git/batten-receipts/board-writes.<branch>, written by the PostToolUse recorder, holding per board write: the id, the updatedAt the tracker returned, and the ready-lint verdict over the body the tracker returned.

    Why a local record at all, since a local record is what made the first draft forgeable. A gate running inside land is a shell task, and no task in mise-tasks/ has a tracker credential — that is claim-check's "agents fetch, gates decide" split and it is not negotiable here, because land runs unattended in the background and cannot call a connector mid-lap. So the gate can read only local state.

    But exactly one fact is genuinely unfetchable, and the first draft confused that with three. Which rows this branch put on the board is recorded nowhere — not on the row, not in git, not on the PR. Everything else about a row (is it refined, when was it last touched) is a property of the row itself. The first draft therefore specified a separate ready-lint receipt and leaned on the issue-search receipt as evidence, and both were unnecessary ceremony around the one record that is actually needed.

    And the forgeability was self-inflicted. A ready-lint receipt is worthless when the agent assembles the payload it lints. The recorder does not have that problem: it fires on the tool result, which is the tracker's own response to the create and carries the full issue JSON. Linting that body, and recording the updatedAt beside the verdict, is evidence the author never touched. No second receipt, and no dependency on making a separately-minted verdict trustworthy.

  • Mechanism as a computable predicate (§2): three sinks, priced in ascending order, and chat is not one of them. A finding met mid-PR has exactly three legitimate destinations, and the gate's whole job is to make the cheapest one the one that finishes the work.

    1. Fix it in this PR. Costs a diff. Nothing to record, nothing to gate, and this is deliberately the cheapest path.
    2. A durable comment on the existing row that owns it. Recorded, and otherwise unpriced. The first draft demanded a search receipt naming the target, on the theory that the row should have been found by looking rather than recalled — which buys nothing: an unfiltered list_issues (optional query, limit up to 250) mints a receipt naming 250 rows, and no receipt can tell a right target from a wrong one anyway. Commenting on the row that already owns a finding is the honest common case and the friction belongs elsewhere. Filing remains gated by issue-search-guard, which is where duplicate-prevention lives.
    3. A new row, groomed to Ready. The recorder lints the body the tracker returned; the gate refuses at land time if that verdict was not green. Costs a search (already required by issue-search-guard) plus a complete Ready block: source of truth, computable predicate, effect, output contract, commit type, test obligation, blockers.

    Sink 3 is deliberately more expensive than most fixes, and that is the entire mechanism. A one-line [tasks."test:bats".env] declaration takes minutes; a Ready block for it takes considerably longer. So the arithmetic that currently favours filing reverses, without anything having to judge whether a given spin-off was lazy.

  • Raising the filing bar is measured to work, in both directions (§2). Where a tracker imposes a structured filing template, monthly incoming volume falls while what survives gets better: median resolution time drops from 381 to 103 days, comment counts from 4.95 to 4.32, and more strongly structured templates further reduce resolution time, reopenings and discussion length. Fewer rows and faster ones is the same effect from both ends, and it is why the price is a Ready block rather than an arbitrary toll.

  • The friction must sit only on the impulsive path (§2). Deliberate friction is legitimate where it prevents an impulsive choice and illegitimate where it obstructs something needed. So: sink 2 stays cheap, since commenting on the right row is the common honest case; sink 3 is expensive but never refused, since a genuinely new finding must always be recordable; and every gate here fails open on anything it cannot establish. A gate that made recording a real finding hard would cause the failure finding-sink-check exists to catch.

  • The recording half must be PostToolUse, not PreToolUse (§2). At PreToolUse a created row has no id yet; the tool result carries it. Measured, because no hook in this tree had ever read a tool result and the documented example is a Write with a flat response. For an MCP tool the result is the content-block envelope, so .tool_response.id does not exist — the recorded shape is [{"type":"text","text":"<the issue JSON as a string>"}], and the key is reached with .tool_response[]? | select(.type=="text") | .text | fromjson | .id. The text carries the entire issue body, so the recorder must extract the id and nothing else (non-negotiable 4). .claude/settings.json already runs a PostToolUse entry, so the event is available and the shape is proven. The body records both a create and a comment — a comment is sink 2 and must be attributable to the branch too — appending to .git/batten-receipts/board-writes.<branch>. It reuses issue-search-guard's decided details rather than re-deriving them: the create-vs-update discriminator (.tool_input.id absent) and the suffix-anchored matcher, since CLOUD-178 measured three live spellings of the same connector and a rule naming one silently matches none of the others.

  • The recorder calls ready-lint; ready-lint itself is unchanged (§2). It is already a pure function of a piped payload and already the authority on a Ready block, so the recorder pipes it the tracker's returned body and stores the verdict. A hook shelling out to a task is normally the cost this repo refuses — but that argument was measured against a PreToolUse firing on every Bash call, and this fires only when a row is created, which is rare enough that the startup cost is irrelevant.

  • The gating half is filed-here-check (§2), called from land beside deferral-check and stopping the lap the same way. For every create this branch recorded, the stored ready-lint verdict must be green. Comments are recorded and not gated. It judges no content and reads no tracker.

  • Do not reuse claimed-keys for any body read (§2). It narrows to closing keywords because it answers which issues does this PR claim, and claiming would demand the PR close the spun-off row — precisely wrong for a genuinely unrelated one. CLOUD-379 and CLOUD-384 record the two being conflated in the other direction.

  • Ship the recorder first, then the gate (§2). The firing rate cannot be estimated retrospectively — the discriminating data has never existed, which is the finding above. The recorder landed alone in feat(hooks): record what this branch put on the board, and whether it was refined #399 (corrected in fix(hooks): a comment records the issue key, never the comment's own uuid #418), and that staging is what makes the gate's input a proven shape rather than an imagined one. It does not buy a measured firing rate, and the original wording promising one was wrong: the record lives under $GIT_DIR, is never committed and dies with the container, so no corpus accumulates across sessions and none ever could.

  • Effect (§3). read. A PostToolUse body writing under $GIT_DIR — machinery, as claim-guard's receipt already is — plus a task reading stdin and receipts, plus one receipt write added to an existing task. No new batten verb and no SURFACE change.

  • Output & exit contract (§5). deferral-check's, unchanged: non-zero stops the lap, pointer-only per non-negotiable 4 — the ids and the missing receipt, never an issue title or body. The recorder is silent on every path and fails open on an unparseable payload, a missing git dir or a detached HEAD.

  • Commit / bump (§6). feat(hooks) — patch until 0.1.0 regardless of type.

  • Test obligation (§7). One suite per gate, because mutant derives its suite as tests/$gate.bats; #MUTANT rows in each and every name added to MUTANT_GATES.

    Shown able to fail in both directions (CLOUD-418): a recorded create with no ready-lint receipt fails, and passes once the row is refined; a create whose recorded verdict is green passes; a recorded comment is never gated. Plus the rows that keep it from obstructing the needed path — a branch with no recorder output at all passes, an empty record passes, an update to an existing row is never recorded, and a fix-with-no-board-write passes untouched, which is the path the design wants to be cheapest.

    The regression case is this session: CLOUD-513 recorded against PR fix: the fast-forward verdict reaches land, and filing joins the gated paths #390 with no Ready receipt at filing time is refused, and passes once groomed.

  • Blockers (§8). None. relatedTo CLOUD-512 — it gates the move to In Review and explicitly excludes the attachment gap this closes as a side effect; CLOUD-323 and CLOUD-338deferral-check, the template and the record of its exemption logic being got wrong; CLOUD-505 — the search receipt this consumes; CLOUD-475finding-sink-check, the gate that prices under-recording and the reason nothing prices over-recording; CLOUD-513 — the measured instance.

Chat is not a fourth sink, and that is a memory property rather than a style rule

A finding stated in prose and never written durably is context-resident state. Long-horizon agents lose exactly that: unbounded context forces truncation and summarisation, attentional dilution degrades recall of any single item as context grows, and material in the middle of a long context is recalled least reliably — which is why an external store holding raw records at full fidelity is the standing recommendation. So a finding left in an aside is not merely undocumented, it is predictably lost, and the container reclaim this session survived is only the abrupt version of the same thing. finding-sink-check already gates this and needs no change here; it is named so the three sinks above are not read as four.

How this gets satisfied without doing the work

Three, found by attacking the mechanism rather than reasoning about it, each with local evidence. Two are closed above by changing the design; the third is a residual, stated with its failure direction.

A ready-lint verdict is forgeable if the agent supplies the payload — closed by moving the lint into the recorder, which lints the tracker's response instead. The finding stands and is why §1 reads as it does; the original remedy (a second receipt, made trustworthy elsewhere) was the wrong fix for it.

The measurement that made the hole real. ready-lint reads a payload the caller assembles. Run against this very issue three times while it was being refined — twice from a local file, once under the literal id CLOUD-NEW for a row that did not exist — it was green every time. So a receipt would attest that some text linted clean under some id, while the filed row is a stub. A toll payable in text nobody filed is not a toll. The first remedy drafted for this was a second receipt made trustworthy elsewhere, which added a dependency instead of removing the hole; moving the lint into the recorder — where the input is the tracker's response rather than anything the author typed — removes it. The same forgery argument is made independently by CLOUD-431 for its own question, which is why that issue is worth reading beside this one.

An unfiltered listing launders sink 2 — closed by dropping the requirement, because it bought nothing. list_issues takes an optional query and a limit up to 250, so a receipt demanding that the comment target be named is satisfied by one no-query call. Nor could any receipt tell a right target from a wrong one. Sink 2 is now recorded and unpriced.

The record survives a branch restart — and that is fail-closed here, which is why it is a residual and not a blocker. claim.<branch> on the branch this issue was written on names CLOUD-230, a claim from an earlier incarnation of the same branch name that outlived two checkout -B … origin/main restarts. The record proposed here is keyed the same way, so it inherits the staleness — but not the failure direction, and that distinction was missed on the first pass. There the defect is a false pass: claim-guard waved through every edit all session on expired evidence. Here a stale entry can only add rows the gate insists on checking; it can never remove one, since a restart does not delete the record and the new branch's own creates are appended fresh. So the worst case is a refusal naming a row from a previous incarnation, whose remedy the refusal message already gives. A gate that fails closed on stale state is the outcome this repo prefers, so the dependency was withdrawn. CLOUD-516 fixes the underlying keying and is worth landing first for its own reasons, but nothing here waits on it.

What this cannot do

It cannot judge the reason, and a minimal-compliance path still exists — a Ready block written to satisfy ready-lint rather than to be worked. The gate cannot refuse that without scoring prose, which is the model verdict rule 3 forbids. But the floor is now a complete Ready block rather than a pasted key, so the cheapest way to satisfy it is close to the work the row actually needs. Re-open predicate: re-open if a spun-off row is found to carry a Ready block written to pass ready-lint rather than to be worked — observable per instance on the row itself, which is the only scope available, since no cross-session window exists.

Gating creates pushes pressure toward commenting instead, and that is accepted rather than fixed. A comment on the row that already owns a finding is a legitimate durable home — cheaper than filing by design, since the friction is meant to sit on the impulsive path and not on honest recording. What makes it acceptable is that the pressure runs toward recording in the right place, not toward silence. It is still recorded, so the ratio is observable: CLOUD-475 records that a comment already counts as a durable home. Re-open predicate: re-open if a branch's own record shows comments standing in for fixes to defects in that branch's own diff — readable per branch at land time, which is the only scope available, since no cross-session window exists.

Acceptance

  • A row created while a branch was live is refined at creation, on the evidence of the tracker's own response, and the lap stops otherwise.
  • Fixing costs less than filing, and the acceptance is arithmetic rather than opinion: the cheapest path through the gate for a defect in the branch's own diff is to fix it.
  • A genuinely new finding is never refused a durable home, and a comment on the right existing row stays cheap.
  • No similarity comparison and no quality score exists anywhere in the mechanism.
  • The gate's deployment safety is structural, not measured, because the observation window this bullet originally promised is unsatisfiable. Verified 2026-08-19: .git/batten-receipts/board-writes.* is one file, nine rows, all from the session that wrote the recorder — zero creates, nine comments. The store is per-clone, never committed, and reclaimed with the container, so there is no fleet-wide window and there never was one. What stands in its place is the gate's own scope: it reads one branch's record inside the session that wrote it, judges only creates, and fails open on an absent or unreadable record — so a wrong verdict costs one lap on one branch, and its remedy is in the refusal message.

Not in this issue

Deciding whether a given spin-off was legitimate — the judgement the gate must never make. The In Review transition gate, which is CLOUD-512's. And retrofitting receipts for branches predating the recorder, which is why the gate fails open on their absence.

CLOUD-418 A new gate is never shown to fail, so a test that cannot discriminate ships as coverage

Why

This repository's most-repeated failure is a claim nothing exercises. land's refusal branch was dead code for months (CLOUD-235). timeout-check's budgets were placeholders that could not fire (CLOUD-352). A shape rule whose pattern was a program could never match and read as coverage (CLOUD-401). Each was caught after the fact.

It happened again, live, while building the landing lease (CLOUD-393). A concurrency test was written for a real race — observe() reading FETCH_HEAD, which is one file per clone while the heartbeat runs beside held/release in the same checkout. The test was green. Then the buggy version was restored to check the test could catch it, and it passed on the broken code too: every process fetches the same lease ref, so a crossed read yields a different generation of the same lease rather than an observably foreign one. The test asserted nothing.

That was found only because someone chose to mutate and re-run — a discipline nothing asks for and nothing checks. The green suite before that check and the green suite after it were indistinguishable.

Root cause. The obligation is stated as "a rule ships with a runnable gate" — a gate that exists. Nothing requires evidence the gate discriminates. A test that passes on both the fixed and the broken code satisfies every rule this repo currently has.

Scope, deliberately narrow. Not mutation testing over the workspace, which is a research project and a large CI bill. The claim here is about mise-tasks/*-check and the guards — the files whose entire purpose is to refuse — where the mutation is usually a one-line inversion and the suite is bats, so a run is seconds.

Refinement — Ready

  • Source of truth (§1). The gate's own suite, run against a deliberately broken copy of the gate. A pass there is the defect; the verdict is an exit code, not a judgement.
  • Mechanism (§3). Undecided between two, and choosing is what Ready needs:
    • Author-side, checked in. Each gate declares one or more mutant cases — a stated one-line corruption and the test name that must go red. A task runs them, and a mutant nothing catches fails. Costs the repo one small fixture per gate; runs locally, off the landing path.
    • Scheduled sweep. A weekly job applies mechanical mutations to mise-tasks/*-check and reports any whose suite stays green. No per-gate authoring, weaker coverage, and it belongs beside branch-age-check in the hygiene sweep, so no new CI minutes.
  • Deliberately not in scope (§2). Mutation coverage of crates/. Different tooling, different cost, different question.
  • Output (§7). Pointer-only: the gate, the mutant, and the test that failed to notice. Never a diff of the mutated source.

Test obligation

The mechanism must catch the case that motivated it: the FETCH_HEAD mutation of mise-tasks/land-lock against tests/land-lock.bats as it stood before the structural assertion replaced it. That pair is a known-good fixture — a real gate, a real mutant, and a real suite that missed it.

Commit / bump (§6): feat(gate) — patch until 0.1.0 regardless of type.

Blockers (§8): none.

Acceptance

  • Every *-check task has at least one mutation its suite is proven to catch.
  • A gate whose suite passes on a broken copy fails.
  • The land-lock/FETCH_HEAD pair is covered as a regression fixture, so the case that motivated this cannot recur silently.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 15 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: 666d32bd-5119-4849-a303-c9192577f7a2

📥 Commits

Reviewing files that changed from the base of the PR and between b45e374 and c9b2e3c.

⛔ Files ignored due to path filters (1)
  • crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap is excluded by !**/*.snap
📒 Files selected for processing (18)
  • .claude/rules/toolchain.md
  • batten.toml
  • completions/batten.bash
  • completions/batten.fish
  • completions/batten.zsh
  • crates/batten/src/cli.rs
  • crates/batten/src/ready.rs
  • crates/batten/src/record.rs
  • crates/batten/src/spec.rs
  • crates/batten/src/surface.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/obligations_bound.rs
  • crates/batten/tests/it/pointer_only.rs
  • crates/batten/tests/it/record_closes.rs
  • man/batten-record-closes.1
  • man/batten-record.1
  • mise.toml
  • policy/obligations-bound.rego
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 5e13bd83-5e85-4020-ab08-d3b06d630b37

📥 Commits

Reviewing files that changed from the base of the PR and between 554eedd and b45e374.

⛔ Files ignored due to path filters (1)
  • crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap is excluded by !**/*.snap
📒 Files selected for processing (35)
  • .claude/rules/policy-modules.md
  • .claude/rules/toolchain.md
  • .serena/memories/workflow/agent-fanout.md
  • .serena/memories/workflow/board-states.md
  • AGENTS.md
  • batten.toml
  • completions/batten.bash
  • completions/batten.fish
  • completions/batten.zsh
  • crates/batten/src/claim.rs
  • crates/batten/src/cli.rs
  • crates/batten/src/config.rs
  • crates/batten/src/doctor.rs
  • crates/batten/src/hook.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/ready.rs
  • crates/batten/src/record.rs
  • crates/batten/src/resolve.rs
  • crates/batten/src/rules.rs
  • crates/batten/src/spec.rs
  • crates/batten/src/surface.rs
  • crates/batten/src/trust.rs
  • crates/batten/tests/it/doctor.rs
  • crates/batten/tests/it/filed_here.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/plan_complete.rs
  • crates/batten/tests/it/pointer_only.rs
  • crates/batten/tests/it/ready.rs
  • crates/batten/tests/it/verdict_vocabulary.rs
  • man/batten-record-plan.1
  • man/batten-record.1
  • mise.toml
  • policy/filed-here.rego
  • policy/plan-complete.rego
  • schema/batten.schema.json

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


📝 Walkthrough

Walkthrough

The change moves bot pull-request handling into batten, adds claim bot, claim carry, and record plan, and supports alternate receipt requirements through checks_any. It adds bot-lane configuration, forge operations, carry validation, receipt accumulation, readiness cutovers, plan-completion and filed-row policies, CLI declarations, completions, manuals, schemas, and integration tests. Workflows now support Renovate and SBOM action lanes and use the new commands. Supporting policy, census, diagnostic, shell-retirement, and repository guidance updates are included.

Merge Risk: ⚪ Minimal · up to b45e3

No actionable merge-blocking risk remains; the PR is merge-ready after normal checks and review.


Note

🎁 Summarized by CodeRabbit Free

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

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

@wenzowski
wenzowski force-pushed the claude/cloud-1295-retire-bot-issue branch 7 times, most recently from f04cf37 to 44d671c Compare September 1, 2026 16:37
@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 17:10
@wenzowski
wenzowski marked this pull request as draft September 1, 2026 17:32
@wenzowski
wenzowski force-pushed the claude/cloud-1295-retire-bot-issue branch 3 times, most recently from 7ca5ba4 to f9f2cde Compare September 1, 2026 19:53
@wenzowski wenzowski changed the title refactor(ci): retire bot-issue onto the engine, and give a carry branch a real receipt refactor(ci): retire bot-issue onto the engine, and close the three rows it surfaced Sep 1, 2026
@wenzowski
wenzowski force-pushed the claude/cloud-1295-retire-bot-issue branch 3 times, most recently from 9ac1576 to b574687 Compare September 1, 2026 21:56
…ressible

`one_exchange` derived the method from body presence — `None => GET`,
`Some => POST` — so PATCH could not be spelled at all. `bot-issue link` needs
it (`PATCH repos/…/pulls/N`, writing a closing key into a bot PR's body), and
that absence is why the program cannot retire onto the engine.

THE DERIVATION CARRIED A REAL INVARIANT, AND IT IS KEPT RATHER THAN TRADED.
Its own comment records what the pairing bought: a caller could not ask for a
GET carrying bytes, or a POST carrying none — two shapes a server answers
differently and neither of which any caller here wants.

Adding a `method` field beside the body would have handed both of those back
as constructible states in exchange for the one method that was missing. So
the method and the body become ONE field instead:

    pub enum Payload<'a> { Read, Create(&[u8]), Update(&[u8]) }

There is no variant for a bodyless write and none for a read carrying bytes,
so the type refuses them rather than a comment asking callers not to. PATCH
arrives as `Update`, and the invariant is now structural where it was a
convention enforced by a two-armed match.

`no_variant_pairs_a_read_with_bytes_or_a_write_without` asserts the shape by
enumeration — every variant writes if and only if it carries bytes — so a
fourth variant breaking the pairing is red. It is the only way a type-level
property can be asserted from inside the crate.

Two construction sites, both updated: `fetch::get` and `mcp::dispatch`.

Refs: CLOUD-1295, CLOUD-1260
The predicate behind the receipt a carry branch will earn. Pure, so the tier
drives every refusal without a git tree, and so what the receipt attests is
the same thing the tests assert.

WHY NOT A BRANCH-NAME EXEMPTION. `sbom-actions-currency` opens its PRs on
`sbom-actions/carry-<timestamp>`, and the cheap fix is to let `verify` accept
that prefix. That would be a password wearing a branch name: anything able to
name itself so would pass, and the receipt would attest nothing about the
change. So nothing here reads the branch name.

What is attested is checkable against the merge base: exactly one tracked
path differs and it is the licence table; every added line parses as
`<repo>@<sha>` plus a licence and a holder; another row for the SAME repo
carries an identical licence and holder, so only the sha differs; and nothing
is removed or rewritten. Together those bound a carry branch to exactly what
the workflow may produce.

TWO CHOICES THAT ARE THE WHOLE PREDICATE, both driven by a case:

APPEND-ONLY IS A PREFIX COMPARISON, not a line-set difference. A set diff
reads a rewritten row as one removal plus one addition and could admit the
addition — which is precisely the edit that must be refused.

A ROW ADDED BY THIS DIFF MAY NOT VOUCH FOR ANOTHER. The known verdicts are
read from the BASE side only; otherwise two unmapped repos vouch for each
other and the branch carries a licence nobody ever judged. Shown able to
fail: seeding from HEAD instead reddens `a_repo_with_no_prior_row_is_refused`
and `a_changed_holder_is_refused_too`.

The offline half is stated as such. Byte-identity of the upstream licence
files is confirmed by the workflow at carry time; this bounds what the diff
may SAY, not what upstream holds. Claiming otherwise would be a receipt
asserting a check nobody performed, which is the defect being ended.

Pointer-only: a refusal names the repo, the path and a line — never the
licence string or the holder it compared, since those are the bytes the table
exists to record.

Refs: CLOUD-1295, CLOUD-1213, CLOUD-629, CLOUD-418

Admits: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: d8e2f2c
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Nothing. One row is appended describing the new module; no existing row is altered, reordered or removed, and no other memory is touched. The map's own gate is what demanded it, so declining would leave the tree unable to commit the module at all.
Admits-answer-precondition: `module-map-check` refuses a new `crates/batten/src/*.rs` that has no row in `.serena/memories/core.md`, and it refused this commit by name — so adding `carry.rs` and adding its map row are one indivisible change. The surface that owns a memory is Serena's `edit_memory`, which is what performed this write; the file is protected because agent context must not influence the rules, and a module map row is a description of code landing in the same diff a reviewer reads.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE was TAKEN rather than rejected — the write went through Serena's `edit_memory`, not a file editor. The refusal is the protected-path gate over the resulting diff, which no surface avoids. R-RESTORE-IT would drop the row and leave `module-map-check` red on a module this same branch adds.
… claim

`sbom-actions-currency` opens a PR that `verify` refuses: there is no claim
receipt, and the agent one attests a refinement no bot performed. #806 landed
with a `--takeover` claim against CLOUD-1213, which was a fake claim, and every
future carry PR meets the same wall.

`batten claim carry` is the honest receipt. It attests DERIVABILITY rather than
a branch name — a prefix exemption would be a password wearing a branch name —
so it mints only when the branch appends rows whose repo the base table already
maps, changing the sha alone, and touches nothing else.

The verb takes no argument on purpose: the subject is the branch's own diff
against its merge base, and a caller that could name its own subject could name
one that is derivable while changing something else.

Also repairs the `snapshots` task, stale since the integration targets moved
under `tests/it/`.

Refs: CLOUD-1295
…the port

The commit this reverts widened `fetch::Call` on the stated grounds that
`bot-issue link` needed `PATCH repos/.../pulls/N` and that the method could not
be expressed, so the retirement was blocked on it.

It was not. `pr_watch` is the landed precedent for forge access from engine
source (CLOUD-1143) and it reads through `gh`, which expresses every method and
which resolves the credential outside this crate — where the alternative would
have put token resolution into it, next to no forge credential row any config
declares. The retirement takes that route, so `Payload::Update` has no caller
and its doc comment states a reason that is not true.

A widened type with no consumer and a false rationale is worse than the
narrower one it replaced, so it goes back.

Refs: CLOUD-1295
…eceipt

486 lines of shell and 323 of bats, six subcommands, all of it GitHub API plus
one local receipt write. `mise-tasks/bot-issue.sh` and `tests/bot-issue.bats`
are deleted; the predicates land as `crates/batten/src/bot.rs` and six new
leaves — `batten pr derive|file|link|ensure|closes` and `batten claim bot`.

TWO HALVES, KEPT APART INSIDE ONE MODULE. The predicates — is this pull request
one of the lane's, which manifests it touched, what Conventional type its
subject declares, whether a body still CLOSES a key rather than merely naming
one — are pure functions with their own unit tier. The `forge` half underneath
is the only thing that talks to anybody.

THE CONSUMER FACTS ARE `[bot_lane]` IN batten.toml, not literals in the crate.
Which repository, which bot logins, which manifests the lane owns, the marker
strings, the tracker's key prefix and the branch prefix — every value the
retired program spelled internally is a config row now, so a grep of
`crates/batten` for a bot's name or a manifest path returns nothing
(non-negotiable rule 1). `document_facts` caught the first draft's doc comment
naming a workflow glob as an example, and that is the gate working. The derived
row's body is a tracked template with `{{...}}` placeholders rather than a page
of markdown inside a TOML value; an unfilled placeholder refuses at
substitution rather than rendering a template artifact into a tracker row.

THE FORGE'S OWN CLIENT, NOT THIS CRATE'S HTTP TRANSPORT. `pr_watch` is the
landed precedent (CLOUD-1143) and `gh` resolves the credential outside the
crate, where `fetch` would have put token resolution inside it next to no
config row that declares one. `policy/spawn-adapters.rego` places `bot` on
`pr_watch`'s own argument. The commit that widened `fetch::Call` for a PATCH
this port turned out not to need is reverted earlier on this branch.

THE EXIT TABLE CHANGED AND THE WORKFLOW FOLLOWS IT. `bot-issue` used 1 for
"refused, not this lane's" and 2 for "could not look"; house style §7 makes a
refusal 2 and a could-not-look 3. `auto-bot-land.yml`'s two steps are repointed
and their `case` arms moved with them — reading 1 as an ordinary refusal there
would have treated a usage error as a pass.

THE THIRD RECEIPT KIND IS WIRED. `verify` now tries `claim`, then `bot`, then
`carry`, so a licence-carry branch no longer needs the fake `--takeover` claim
`DO-NOT-CLOSE CLOUD-1213`, without which every carry pull request stalls on
`closing-key-check` exactly as #806 did and needs a human to patch it.

WHAT IS NOT DONE, AND WHY IT IS FILED RATHER THAN FIXED. The plan called for a
`checks = ["carry"]` row beside `claim-needs-receipt`. That row is wrong: a
receipt rule's `checks` is a CONJUNCTION, so a second row is a second AND and
would deny every ordinary write on every branch. `verify`'s shell body can
express the disjunction and the mediated gate cannot — a live false positive
for the bot receipt since CLOUD-693, which this change's third kind inherits.
CLOUD-1297 owns it, with the disjunction as its §2.

TWO GATES CAUGHT REAL DEFECTS IN THIS CHANGE AND BOTH ARE FIXED HERE.
`no-appeal-to-authority` refused `carry.rs` and its tier for naming a real
action repository as fixture data — a third party's name in the crate, which is
the class the rule exists for, and the fixtures now use neutral names.
`module-layering` refused both new modules as unplaced, which is the coverage
clause working: `bot` and `carry` are placed with their reasons.

ONE EDIT IS DECLINED RATHER THAN FORCED, and CLOUD-1299 owns it. Retiring the
program makes `verify`'s no-receipt remedy name a task that no longer exists —
but `tests/verify.bats` asserts that message contains `bot-issue receipt`, and a
governed suite has two landable shapes, neither of which is an edit.
`repoints_at_the_declared_invocation` cannot admit it either: it requires the
replaced span to be a PATH reference, and a task name plus a subcommand is not
one. So the remedy names the live verb FIRST and keeps the old name as the thing
it replaced — true, useful mid-transition, and satisfying the pin — with the
reason at the site rather than left to read as a slip.

THE LEDGER. Two file arms and 22 case arms, `kind:verb` on each because the
lane needs stdin, spawns with its own arguments and performs writes — none of
which a tree-scoped module may do. `bot-issue` leaves `$MUTANT_GATES` and needs
no `#MUTANT-EXEMPT`: the census enumerates `mise-tasks/*.sh` and `policy/*.rego`
— `mutate::subjects`, since `main` retired `mutant-census` onto the engine under
this branch — so a deleted program is not in the census at all. The arm
carries a `runs:` field naming `batten claim bot` — ONE arm per deleted path,
which `V-RETIREMENT-AMBIGUOUS` requires, so the field names the only invocation
a GOVERNED caller loses; the lander's two workflow steps are ungoverned and
their repoints are free. Its two
declared mutations are re-homed as named cases —
`idempotence_a_second_call_on_the_same_pr_files_nothing` and
`a_key_named_but_not_closed_is_refused` — each carrying the mutation it stands
for in its comment.

THE LOAD-BEARING CASE IS REPLAYED, NOT ASSERTED. The dying suite ran the REAL
`ready-lint` over the REAL derived payload, because "the derived block is
checkable by the same gate that checks a human's" is the claim that makes a
mechanical row honest. `the_derived_block_passes_ready_lint` does the same over
this repository's committed template and its committed `[[pattern]]` grammar —
and it earned its place immediately, catching that `prettier` reshapes the
template's bullets on every `mise run fmt`.

A KNOWN LAG, STATED RATHER THAN HIDDEN. `auto-bot-land.yml` installs `batten`
from the latest RELEASE, so its two repointed steps fail loudly with "command
not found" between this merge and the next release. That is the same gap
CLOUD-1143 recorded for `checks-green`, and it fails the job rather than
passing silently.

126 suites after the deletion, 561.9s serial on this container, regenerated on
the rebased base rather than the one this change was written against — `main`
retired several more suites in between, `mutant`, `mutant-census` and
`session-start` among them, and a corpus derived from an older report publishes a
cost for a suite that is gone. The saving this change can claim is
`tests/bot-issue.bats`' own 2.9s share of the corpus it was measured in, 0.3%;
the totals differ by far more across runs, and that is those other retirements
plus machine noise rather than anything this branch did.

Refs: CLOUD-1295
Refs: CLOUD-1164
Refs: CLOUD-1213
Refs: CLOUD-1297
Refs: CLOUD-1299

Admits: 1c68cb4a5d20efa873c0ed2b7564b7cfdf53c0b6ebb9c79e211eea4b8445bcc5
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a11e917
Admits-epoch: 650a76ba70a8b5645ffc58aedb1bdde72394e39026b5c1391a926934ea49702c
Admits-author: alec@wenzowski.com
Admits-prev: c7d8beede76f452a09d1a6c47a615b728f2876a60e055ef065975a27903f003f
Admits-answer-lost: mise-tasks/bot-issue.sh stays alive as a second implementation of predicates the engine now owns, which is the state policy/shell-retirement.rego's V-SHELL-RULE-EDITED exists to end -- the ported verbs cannot read a lane nobody declared, so refusing this write refuses the retirement rather than this line of it.
Admits-answer-precondition: The bot lane's consumer facts -- which forge repository, which bot logins, which manifests the lane owns, the marker strings, the tracker's key prefix and the branch prefix -- are exactly the values non-negotiable rule 1 forbids in crates/batten. Retiring mise-tasks/bot-issue.sh onto `batten pr derive|file|link|ensure|closes` and `batten claim bot` moves the MATCHER into the engine and leaves those facts here, so batten.toml is the owning surface for them and no other surface can carry them. The write is one additive [bot_lane] block, visible in this pull request's own diff, which is where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a consumer's policy facts; there is no other surface that owns them. A local override cannot carry them either: adding a login to `bots` or a path to `owned_manifests` turns a refusal into a filed row, which is a weakening dressed as an addition and house style section 8's raise-only rule does not admit it. R-RESTORE-IT does not apply because the write is additive and deliberate rather than an accident to undo -- restoring the file would delete the table the ported verbs read.

Admits: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The bot lane's lander breaks outright at its next tick: both steps call a task the retirement removed, so no bot pull request gets a row and none is landed. The alternative is keeping mise-tasks/bot-issue.sh alive as a second implementation, which is the state V-SHELL-RULE-EDITED exists to end.
Admits-answer-precondition: auto-bot-land.yml invokes `mise run bot-issue ensure` and `mise run bot-issue closes`, and this change deletes mise-tasks/bot-issue.sh. Leaving the calls would leave the lander invoking a task that no longer exists, so the workflow is the owning surface for its own steps and there is nowhere else the repoint could be made. The write is two invocation repoints -- `batten pr ensure` and `batten pr closes` -- plus the comment naming the retired program, and nothing about what the workflow decides changes.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/auto-bot-land.yml IS the owning surface for the lander's own steps; a workflow's run lines cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate repoint at a successor this same commit lands, and restoring the file would leave a workflow calling a deleted program.

Admits: a3bb7b559f5118c0fe2cc4e4c8e2336e2bcbf1262a0df2578070e44446a20751
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .github/workflows/sbom-actions-currency.yml
Admits-head: a11e917
Admits-epoch: 8e6d5f2f19b383997365f6b72ff52816d72c7029208dbb81a40389acbd65e67f
Admits-author: alec@wenzowski.com
Admits-prev: e03b883be1a24354b6eaeedb59a2c39f16052d832ef6dde2ceab292ca964fa5b
Admits-answer-lost: Every carry pull request `sbom-actions-currency` opens stalls on `closing-key-check` exactly as #806 did, and each one needs a human to patch its body before it can land -- which is the manual step CLOUD-1213 exists to remove, on the lane whose blockage this whole change is about.
Admits-answer-precondition: The generated pull request body carries `Refs: CLOUD-1213` and no closing marker, so `closing-key-check` reads it as a body naming a key non-closingly and stalls the landing. Measured on #806, where the body was patched by hand to get it through -- the defect repeating on every future carry pull request. The body is generated by this workflow's own `gh pr create` step, so the workflow is the owning surface for it and no other surface can set it. The write is one added line, `DO-NOT-CLOSE CLOUD-1213`, in the same printf list, visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because .github/workflows/sbom-actions-currency.yml IS the owning surface for the body it generates; a workflow's own `gh pr create` arguments cannot be declared anywhere else. R-RESTORE-IT does not apply because the change is a deliberate one-line addition rather than an accident to undo, and restoring the file would put back the body that stalls.

Admits: 1588b760e38a64a36f5aaef031bd5b3e8f5e1160c0f94eec486b41dbca5d335c
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/core.md
Admits-head: a11e917
Admits-epoch: a5e521241d3b4c861cd0ffb0813ac2c5eb0bfd1b3b364e13418f67888e3de545
Admits-author: alec@wenzowski.com
Admits-prev: 121e5c62ce26f6caa46a19393e76ae86d200b8b113fccdaf2bb1196df372865b
Admits-answer-lost: `bot.rs` lands with no map row, `module-map-check` fails the gate, and the retirement cannot commit at all -- so refusing this write refuses the whole change rather than this bullet of it. A reader looking up what `bot.rs` owns would find nothing where every other module is described.
Admits-answer-precondition: `module-map-check` refuses a `crates/batten/src/*.rs` module with no row in `.serena/memories/core.md`, and this change adds `crates/batten/src/bot.rs`. The memory IS the module map's authority -- AGENTS.md points there for the per-module layout precisely so the tree is not restated in a budgeted file -- so there is no other surface a row could go on. The write was made through Serena's `edit_memory`, which is the sanctioned route, and it is one added bullet visible in this pull request's diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `.serena/memories/core.md` IS the owning surface for the module map; AGENTS.md defers to it by name and `module-map-check` reads it as the authority. R-RESTORE-IT does not apply because the write is one additive bullet describing a module this same commit introduces -- restoring the file would leave the map missing a row the gate demands.

Admits: cbb7e7b9a66ad1ec32b1d2e5f2df9e67efc920620236ceb72e667aa6e772fdc7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/spawn-adapters.rego
Admits-head: 6da96ce
Admits-epoch: d8992f36146d4a8f3bd0250644ab0624b29c8cc7abd4b5640b4c4aa2c6c5efe6
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The rebase cannot complete, so the retirement cannot land at all. Taking `main`'s side alone leaves `bot` unplaced and `spawn-adapters` refuses `crates/batten/src/bot.rs`; taking this branch's side alone deletes `mutate` and reintroduces the refusal on the module `main` just landed.
Admits-answer-precondition: This is a REBASE CONFLICT RESOLUTION, and both sides of it are additions to the same set literal. `main` placed `mutate` when it retired `mise-tasks/mutant.sh`; this branch places `bot` when it retires `mise-tasks/bot-issue.sh`. Neither adapter can be dropped -- `spawn-adapters` refuses a spawn in a module the table has not placed, so losing either side reintroduces the refusal the other change already answered. The resolution is the union of the two rows plus both reasons, which is what the file would hold had the two landed in sequence rather than concurrently. There is no other surface: the adapter table IS this module.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because `policy/spawn-adapters.rego` IS the owning surface for the adapter placement table; the set literal cannot be declared anywhere else. R-RESTORE-IT does not apply because restoring the file to either side is precisely what loses one of the two placements -- there is no version of this file that carries both without this write.
Windows resolves an executable by PATHEXT, so the extensionless
`#!/usr/bin/env bash` stub the suite places first on PATH is not a
candidate at all. It is skipped, and a runner with the real client
installed resolves to that instead — two cases reported the port broken
(exit 3) where the port was fine and the stub had never run.

session_provisioning.rs and connector_allow_door.rs gate their suites on
the same rung for the same reason, and the retired tests/bot-issue.bats
stubbed the same client the same way and never ran on Windows either, so
nothing is narrowed that was covered. A .cmd twin of the dispatch would
be a second authority over what the stub answers.

Refs: CLOUD-1295
`Rule::checks` is a conjunction with no disjunctive spelling, so a row
could not say "this receipt OR that one". `verify`'s own body has always
accepted a claim, a bot or a carry receipt and treated any one as enough;
`claim-needs-receipt` demanded claim alone, so a branch legitimately
carrying a bot.<branch> receipt was denied on every write while holding a
valid attestation - a live false positive since CLOUD-693 that
CLOUD-1295's carry kind inherited.

A second receipt row could not express it: a second row is a second AND,
so the obvious spelling would deny every ordinary write on every branch.
Hence a column. `checks_any` sits beside `checks` and a row carrying both
is their conjunction, which is CNF and so expresses what neither column
alone can. `checks` stops being unconditionally required on the kind -
the same conditional-column move `pattern` and `verdict` already make -
and a row naming neither is refused.

Every fact-RESOLUTION site now reads `Rule::receipt_names()`, which
yields both columns. That is the dead-gate half: a site walking `checks`
alone would leave alternation names unresolved, an unresolved name reads
Missing, and the row would deny every call it selected while reading as
configured. Adjudication keeps the columns apart, in one extracted
function so the two call sites cannot drift.

The alternation is still a gate - a branch carrying none of the three is
denied exactly as before - but admitting an alternative does lower a bar,
and `config-lint` reads it as such against origin/main.

Weakens: rule[claim-needs-receipt].checks rule[claim-needs-receipt].checks_any

Admits: 40766b5e91c5f669d325302e8f016616ea07df9f2ddcea5b427e45b6271d3a28
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: e2b31cc
Admits-epoch: e15dee1ebb84e7a0fe39ff59b6620fd9e766a471468936be5c6a15800491a04e
Admits-author: alec@wenzowski.com
Admits-prev: 29c18e3dd204c02bc64809928b5513310a0d41b26913cb6b89029cdff01c3345
Admits-answer-lost: The false positive CLOUD-1297 records stays live: a branch carrying a valid `bot.<branch>` or `carry.<branch>` receipt is denied on every mediated write while holding a real attestation. `verify` accepts all three kinds and this row accepts one, so the two authorities on "is this branch claimed" disagree, and the weaker one is what an agent meets first. CLOUD-1295 has just added the third kind that inherits it.
Admits-answer-precondition: The change is a `[[rule]]` row's own predicate: `claim-needs-receipt` moves from `checks = ["claim"]` to `checks_any = ["claim","bot","carry"]`. A rule row IS the committed authority — no verb writes a row's predicate on an author's behalf, so there is no owning surface to route through. The write is the row a reviewer reads to learn what the gate demands, so it lands in the diff where it is visible.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because `batten.toml` is itself the owning surface for a `[[rule]]` row; the engine half of this commit only makes the column expressible and cannot declare the row. R-RESTORE-IT is rejected because restoring the committed bytes reinstates the defect the row exists to remove, which is the null change rather than a route.

Refs: CLOUD-1297, CLOUD-693, CLOUD-1295
CLOUD-1297's second tier. Five cases on the same row the file already
drives as a conjunction, in a separate policy constant so both spellings
stay exercised — folding them into one fixture would leave whichever
spelling it did not use untested.

All three alternatives are driven, `claim` included: the column must
still admit what the conjunction admitted, and a suite checking only the
additions would pass over a column that had silently replaced the
original. The vacuity case is the one the column most needs — an
alternation fails by being satisfied by nothing, which is an allow
wearing a gate's name — and a receipt of an unnamed kind proves the row
admits the kinds it names rather than any receipt in the store.

Shown able to fail: reading `checks_any` as an empty alternation, which
is the dead-gate shape this column is most at risk of, reddens four of
the five including the vacuity case.

Refs: CLOUD-1297
`is_retired_reference` had four arms and every one of them resolved a
PATH: the repo-relative one, a constructed sibling, a directory in a
variable, the whole path in a variable. A caller that names its callee as
a TASK carried none of them, so retiring a program such a caller names
had no landable shape at all.

Measured on `tests/verify.bats`, which pins the `verify` remedy's PROSE:
it asserted the refusal names `bot-issue receipt`. Retiring the program
that names makes the remedy false; changing the remedy fails the
assertion; and editing a governed `.bats` is refused with no override
route. The two landable shapes were retire the suite whole — a different
unit's work, its subject is `verify` — or leave it alone. CLOUD-1295 took
neither and kept the dead task name alive inside the live remedy.

Arm 5 admits it, and the arm is used here rather than merely added: the
suite and the remedy are both repointed at `batten claim bot`, and the
workaround comment in `mise.toml` is deleted rather than left explaining
a constraint that no longer exists.

It sits on `is_retired_reference` rather than `is_retired_reference_by_
text`, which is the narrowing — the `_by_text` family is what decides
whether a VARIABLE holds the retired path, and a task name is not a path,
so admitting one there would have widened arms 3 and 4 for no reason
anyone measured.

It is not the `contains` licence the module refuses. The naming form is
located and then BOTH SIDES of it are matched whole against anchored
registry rows: `shell-task-runner-prefix` before it, empty or exactly the
runner invocation, and `shell-task-subcommand` after it, empty or
space-separated lowercase words. Every byte of the span is accounted for,
so `mise-tasks/` fails the prefix and a path span cannot be read as a
task span. The clause's other narrowings are untouched: the span is still
derived from the diff, and the target still comes from `invocations_for`.

Admits: 7152141e1e94ec0e30d2b38d724c8d79a2b978739003dbdd8b3899ae41bddfca
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: f9f2cde
Admits-epoch: f95421cc06dae85d936bb7c0b286b70258fb656bb5364aa9bd4ea9c0c16864c0
Admits-author: alec@wenzowski.com
Admits-prev: 40766b5e91c5f669d325302e8f016616ea07df9f2ddcea5b427e45b6271d3a28
Admits-answer-lost: CLOUD-1299 stays open and the retirement it blocks stays half-done. `tests/verify.bats` pins the `verify` remedy's prose to `bot-issue receipt`, a task name plus subcommand, which `is_retired_reference` could not recognise — so CLOUD-1295 shipped a dead task name kept alive inside a live remedy because a frozen suite demanded it. Without these rows the new arm cannot be spelled at all: the alternative is an inline regex the loader refuses.
Admits-answer-precondition: The change adds two `[[pattern]]` rows, `shell-task-subcommand` and `shell-task-runner-prefix`. A pattern registry row IS the committed authority: `.claude/rules/policy-modules.md` refuses an inline regex in an in-repo module at load time, so a module that needs a pattern has no route except a row here. There is no owning surface to go through and no verb that writes one. Both rows land beside the existing `shell-script-directory` pair the same module already reads, where a reviewer comparing them sees the anchoring property they share.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because `batten.toml` is the owning surface for a `[[pattern]]` row, and the whole point of the registry is that one concept has one spelling there rather than nineteen inline. R-RESTORE-IT is rejected because restoring the committed bytes leaves the module unable to name the pattern its new arm matches against, which is the defect rather than a route away from it.

Refs: CLOUD-1299, CLOUD-1219, CLOUD-1224, CLOUD-1295
`reclaim-census report` classifies the PREVIOUS boot, which is immutable
history: recording this boot does not move it, and no landing completed
here changes what the last container was doing when it died. So on a
container whose predecessor was reclaimed mid-landing the verdict is TRUE
and repeated at every session start for the life of the container — and
after the first read it is exactly the noise CLOUD-891 removed. The
handler's own comment already made that argument for the negative
readings and never applied it to the positive one.

The mark is keyed to the boot time and sits beside the census log rather
than in it, with both paths taken from the census's own `log-path` and
`boot` accessors so this body holds no second opinion about where the
store lives. Keying on the boot is what makes it self-clearing: a new
container has a new boot time, so its own first reclaim verdict is
reported rather than suppressed by a mark its predecessor left.

Every failure path still reports. If the mark cannot be resolved or
written the verdict is printed, because a sensor that goes quiet when its
bookkeeping breaks has deleted the instrument CLOUD-451 built —
suppression is what must fail closed, never the report.

The whole fix is in the task body because `mise-tasks/reclaim-census.sh`
is governed by `shell-retirement` and is not this row's to edit.

The tier is Rust because the case that would have caught this died with
its suite: `main` retired the session-start hook into handler rows and
took `tests/session-start.bats` with it, a replacement `.bats` is refused
by `V-SHELL-RULE-ADDED`, and `tests/reclaim-census.bats` is governed at
head. It drives the real task body over a fixture `GIT_DIR` with an
injected `BATTEN_BOOT_TIME` — isolation the census already affords,
because a suite reading this container's own store would suppress the
live verdict a human still needs.

Shown able to fail: dropping the mark comparison, which is the shape that
reintroduces the defect, reddens both suppression cases.

Refs: CLOUD-1301, CLOUD-451, CLOUD-891, CLOUD-1295
Two gates caught the previous commit and both are right.

`inline-task-bodies-not-growing-basic` counts `run = """` in mise.toml,
and the multi-line body added one. The ratchet's own comment says which
way out: a one-line `run = "..."` shim is the campaign SUCCEEDING, and
two of its four replayed firings were exactly that. So the body collapses
to the single-line form the task already had, with the reasoning staying
in the comment above it where it does not count. A new `mise-tasks/`
program was never the alternative — `V-SHELL-RULE-ADDED` refuses one.

The two spawns in the new tier carry `#[expect]` inventory rows rather
than being removed. `git` because the census resolves its store with
`git rev-parse`, so a hand-built `.git` would test a path the program
never takes; `mise` because the subject IS a task body, and spawning the
engine instead would assert over a decision this row deliberately does
not put in the engine.

Behaviour re-driven on this container after the collapse: reports on the
first session, silent on the second.

Refs: CLOUD-1301
The `Weakens:` trailer on the `checks_any` commit named two KEYS and no
smell. `lint::admissions` compares against `<smell-id> <key>`, exactly the
pair a reader already sees in the pointer line, so that trailer admitted
nothing and `config-lint` refused the tree. It was also separated from the
`Refs:` line by a blank line, which puts it outside the final paragraph
and stops git reading it as a trailer at all — so it was invisible twice
over.

The groomed half was missing too, and that is the half house style §8
actually turns on: an admission needs evidence from two moments — a
trailer that travels with the change, and a body that named the same pair
before the work started, copied into the branch's claim receipt by
`claim::mint`. A trailer alone is the self-issued permit §8 refuses, and
CLOUD-841 made a groom that looked and named nothing REFUSE rather than
fall through. CLOUD-1295's Ready block now carries the clause its own §8
predicted when CLOUD-1297 was filed, and the receipt was re-minted from it.

An empty commit because the trailers are the artifact: `admissions` scans
every commit in the range, so they need not sit on the commit that
performed the weakening, and rewriting that commit would rebase history
other work on this branch is already stacked on.

Weakens: rule-predicate-changed rule[claim-needs-receipt].checks
Weakens: rule-predicate-changed rule[claim-needs-receipt].checks_any
Refs: CLOUD-1297, CLOUD-1295
…tions

Three things the rebase onto current `main` exposed, none of them a merge
artifact.

`every_rule_column_carries_a_fact_verdict` reds on a `Rule` column nobody
has classified, and `checks_any` was one. It declares the SAME fact as
`checks`: the two differ in how they are adjudicated — all of one, any of
the other — and not at all in what must be resolved to adjudicate them,
because "any one is valid" cannot be answered without asking about each.
Declaring it not-fact-bearing would have said the alternation's names need
not be resolved, which is the dead gate CLOUD-1297 exists to have avoided,
so this entry is what makes `Rule::receipt_names` honest at the
acquisition layer the way that helper does at every resolution site.

`the_emitted_surface_is_exactly_the_committed_row_set` crossed the line
ceiling because `main` added rows while this branch added six. The
expected set moves into `committed_rows`, with each row's reasoning
travelling WITH it — a reader who has to look elsewhere for why a row is
spelled as it is has the problem the extraction was meant to solve.

`census_fixture` was split twice independently: `main` extracted the
config text, this branch extracted the repository chain, and both survived
the merge as duplicate definitions. They compose rather than compete — the
const holds what each verb needs DECLARED, the function holds the tracked
files and the two commits a diff-shaped verb needs — so both are kept.

The corpus is re-measured over the rebased tree, since the conflict
resolution took main's copy and that one still bills a suite this branch
deletes: 122 suites, 511.8s serial.

Refs: CLOUD-1297, CLOUD-1295
`target-prune` refused every lap on this container: `[prune.warm] was
measured against a tree that no longer exists` — declared 152, live 164,
tolerance 10. Free space was 9648MB against a 7938MB floor, so nothing
about the refusal was disk; it was the basis-drift arm doing its job over
this branch's three added suites plus main's own.

`count` moves to 164 as the gate instructs. `measured` already reads
2026-09-01 from that same day's earlier move, so the pair the block
demands is satisfied by the count alone rather than by back-dating
anything.

THE FLOORS DELIBERATELY DO NOT MOVE, for CLOUD-1210's reason restated
rather than re-derived: grouping the test targets severed the premise
that a tracked FILE proxies a linked STEM — 147 linked artifacts became
4, `target/debug` went 7.76 GB to 2.05 GB — so both floors have sat far
above what the tree needs ever since. A refreshed count therefore cannot
make them too low, which is the direction that fails silently. Moving
them DOWN needs the independent measurement the block names, and that is
CLOUD-1158's.

Admits: b28fd480c8126febacd3db96118787d32ebab604e9dce98f869210441887098e
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: c3eb480
Admits-epoch: bc1480e69c82d83fe97bbc27de2fc23febec91b9b02b69aa024f76f3ed7c187a
Admits-author: alec@wenzowski.com
Admits-prev: 7152141e1e94ec0e30d2b38d724c8d79a2b978739003dbdd8b3899ae41bddfca
Admits-answer-lost: `verify` cannot run at all on this container. `target-prune` refuses every lap on "[prune.warm] was measured against a tree that no longer exists" (declared 152, live 164, tolerance 10), so no receipt is written and `land` cannot reach CI — with free space at 9648MB, well ABOVE the 7938MB floor, so nothing about the refusal is about disk. The drift is this branch's three added suites plus main's own, which is the gate working; leaving it blocks the branch on a stale number rather than on a real bound.
Admits-answer-precondition: The change refreshes `[prune.warm.basis]` and `[prune.cold.basis]`'s `count` from 152 to the live 164, which the gate itself demanded in as many words: "Re-measure the floor and move `count` and `measured` together." A basis count is a committed-authority value — no verb writes one, and the refusal names this file as the thing to edit — so there is no owning surface to route through. The write is two integers plus the comment recording why the floors do not move with them, beside the block that already carries that reasoning for the previous refresh.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because this file IS the owning surface for a `[prune.*.basis]` row, and the refusal's own remedy names editing it. R-RESTORE-IT is rejected because restoring 152 restores the refusal: the count describes a tree that no longer exists, and no amount of restoring makes it true again.

Refs: CLOUD-1295, CLOUD-1210, CLOUD-1158
The rebase onto current `main` took its RESULTS.md at the conflict, which
still bills `tests/bot-issue.bats` — a suite this branch deletes — and
misses main's own retirements. Re-measured over the tree that ships: 121
suites, 472.5s serial.

The total is not this change's to claim: several retirements separate it
from main's reading, this branch's and main's alike.

Refs: CLOUD-1295
CLOUD-1213's first acceptance clause. `auto-bot-land` merged only
`renovate/**`, so every carry PR needed a person — which is what produced
#806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove.

THE PAIRING IS THE POINT. The two lanes have different authors: Renovate
opens its own PRs, and the carry lane is opened by `sbom-actions-currency`
under the default token. A single `BOT_LOGIN` no longer answers, and the
lazy widening — any known prefix against any known login — would admit
`renovate[bot]` on a `sbom-actions/` branch and this workflow's own token
on a `renovate/` one, neither of which either lane can produce. `LANES` is
therefore a table of PAIRS and a PR must satisfy one row whole.

CLOUD-867's origin test is untouched: `head_repository.full_name` is still
what a fork cannot forge, still checked separately, and the prefix stays a
filter rather than the trust boundary.

Driven over a six-PR fixture before landing, and it caught a real bug: the
first spelling read `.prefix` inside a pipe where `.` had already rebound
to the branch string, which `jq` refuses at runtime — a workflow that
would have errored on its first tick. With the lane bound, both lanes are
admitted, both cross-pairings are refused, a fork-headed PR is refused and
a human's branch is refused.

What still gates a carry PR is unchanged and is not review: the full
required check set must be green, and `batten claim carry` bounds what the
branch may contain — only the licence table differs, every added row names
a repo the base already maps with an identical licence and holder so only
the sha moves, and no other tracked path differs from the merge base.

Admits: b93a99d9da1523b9d9bd398f5b8c302d989357d8214bffe6e09c5e6aa30e9d12
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: .github/workflows/auto-bot-land.yml
Admits-head: 0e089a9
Admits-epoch: 25c305c35f01ee4c99bceeacd1efc47509463adf09d3e5923196f59089cc2d98
Admits-author: alec@wenzowski.com
Admits-prev: ddd46be8d9ae384d97c38556832349bfc8dc319895761edb2255894b821b3763
Admits-answer-lost: CLOUD-1213's first acceptance clause stays open and every licence-carry PR keeps needing a person to land it — which is what produced #806's fake `--takeover` claim, the defect CLOUD-1295 exists to remove. The lane can now be landed honestly either way, so what is lost is the automation rather than the correctness; the human asked for the automation explicitly.
Admits-answer-precondition: The change extends this lane's on-green auto-merge from `renovate/**` to `sbom-actions/**`, which is a workflow trigger scope, a job `if:` and the resolve step's author test — all of them inside this file and expressible nowhere else. No verb writes a workflow, and the branch scope has to sit on the trigger rather than only in the `if:` for the reason this file already records (CLOUD-493: a job condition is evaluated after the run exists, so 1131 runs in 25 hours were created only to skip). It lands in a pull request, which is what the class asks for, and the diff is what a reviewer reads to see exactly which branches can now merge unattended.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because a GitHub Actions workflow IS its own owning surface — there is no generator and no config table that emits this file. R-RESTORE-IT is rejected because restoring the committed bytes restores the single-lane scope, which is the thing being changed rather than a fault to undo.

Refs: CLOUD-1213, CLOUD-1295, CLOUD-867
@wenzowski wenzowski changed the title refactor(ci): retire bot-issue onto the engine, and close the three rows it surfaced refactor(ci): retire bot-issue onto the engine, and close every row it surfaced Sep 1, 2026
@wenzowski
wenzowski force-pushed the claude/cloud-1295-retire-bot-issue branch from b574687 to 554eedd Compare September 1, 2026 22:41
`stop_nudges` rule 5 has asked the right question at the end of every turn
since CLOUD-1051 — is this row genuinely independent work, or a punt you
could close here? It is a nudge, so an agent reasons past it: answering
wrongly costs nothing and the answer dies with the turn.

Measured 2026-09-01: four deferrals in one session, each with a
principled-sounding blocker, every one of them false. Three were invisible
to `filed-over-own-diff` because their §1 named paths outside the diff,
which `cites_only` exempts by design and rightly so for a refusal about
proximity. The fourth was caught only because it named the diff. The
detector was a human asking twice.

`filed-and-left-open` is the third arm. It reads the rows this branch put
on the board, subtracts the ones the PR body closes and the ones recorded
before the branch's base, and reports what is left. It classifies nothing:
whether a spin-off was legitimate is still the judgement no gate makes, and
the author still makes it — in an admission whose articulation CLOUD-1278
binds into the commit message, where a reviewer reads it.

CLOUD-514 ruled this half out in terms that were right on the day. The
premise is obsolete rather than the reasoning: the shape it lacked — deny
over an object, with the only exit an explanation written into history —
exists since CLOUD-1051 and CLOUD-1278.

Partitioned, not nested. `filed-over-own-diff` requires `not
cites_only(id)` and this requires `cites_only(id)`, so no row earns both
and a reviewer never sees two findings for one cause. Drafted without that
requirement the arm was strictly wider than the proximity one, which broke
the module header's own invariant.

Three could-not-looks guard it, each a different question: an unread PR
body (the closing remedy has nowhere to be written yet), an empty diff (a
branch holding nothing open deferred nothing), and a record with no §1
column (the partition cannot be evaluated, so the row stays judged as
before). `closes -` and `closes 0` stay distinct, which is what
`zero-is-a-count` exists for.

Refs: CLOUD-1311

Admits: 067783fc36bd1a49703ad0dc249fb95875ef1f9f60e06505e51a155e603e4509
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/filed-here.rego
Admits-head: be5a33c
Admits-epoch: 781d35649dedb150ccb69e890c6aa482c7393233f5ffe1dce100609afd6150fb
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: CLOUD-1311 cannot be built at all. The punt sweep stays `stop_nudges` rule 5 — a nudge with no exit code — which is the defect measured on 2026-09-01: four deferrals, each with a principled-sounding blocker, every one of them false, and the detector was a human asking twice rather than any mechanism.
Admits-answer-precondition: A registered .rego module has no owning verb: the module file IS the surface that declares the predicate, so there is no route that adds a third `violation` arm to policy/filed-here.rego except writing the file. The write lands in a reviewed PR for CLOUD-1311, where `mise run policy-test`, the compiled tier in crates/batten/tests/it/filed_here.rs and the declared `#MUTANT` rows all judge it before it can bind anything.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because no such surface exists for this object: batten declares no verb that writes a predicate into a registered module, and the redirect's own remedy is to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed or retracted — this is an addition to a module that keeps both existing arms intact, with the second arm narrowed only so the third cannot subsume it. Stated for the reviewer because it is the reason this class is protected at all: the arm being added judges the author who is adding it, so the diff and not my account of it is the thing to read.

Admits: 4265150829ab6e4bb6ac8d3976cda321d94ca094835872fd4dceb6759672c6c1
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: be5a33c
Admits-epoch: 781d35649dedb150ccb69e890c6aa482c7393233f5ffe1dce100609afd6150fb
Admits-author: alec@wenzowski.com
Admits-prev: bc0321f2fc7cbb652a826c1d4f8928972e3d7c212a817ed94c9af37e90c74973
Admits-answer-lost: CLOUD-1311's arm is unloadable and therefore unlandable. The module edit already made is dead code until this row exists, so declining here leaves the tree strictly worse than not starting: a module that fails to load disarms `filed-unrefined` and `filed-over-own-diff` too.
Admits-answer-precondition: A `[[verdict]]` row is only expressible in batten.toml: the registry IS the surface, and `policy/filed-here.rego`'s new arm cannot load at all until `V-FILED-AND-LEFT-OPEN` is declared there — a module raising a token no row declares is refused at load. So the write to the authority is not merely the shortest route, it is the only one, and it lands in the reviewed PR for CLOUD-1311 where `mise run config-lint` and `mise run batten-check` judge it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a verdict class — there is no verb that registers one, and the path's own redirect says to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed: this adds one `[[verdict]]` row with its routes and touches no existing row. The write is additive and strictly raise-only — a new deny class with a declared override precondition — so it cannot weaken any gate, which is the property house-style §8 asks of a config change and the one a reviewer should check in the diff.
Two cases failed on one cause, and it is the fixture rather than the arm:
`install_module` writes `policy/filed-here.rego` into the scratch working
tree and `base_delta` walks the tree rather than the index, so every
fixture here has a non-empty delta by construction.

`a_branch_with_no_diff_judges_no_row` therefore cannot exist at this tier.
It stays as the module's own `test_` rule and this file records why the
compiled tier cannot hold it — the mirror of the rule that a `with input
as` case cannot prove the engine builds a shape. Deleting the assertion
without the note would have left the guard untested in both tiers while
looking covered in one.

`a_row_recorded_before_the_file_was_touched_is_still_caught` was asserting
silence before the touch. With the third arm that record is a row left
open, so the case now asserts the row MOVING between the two arms —
`filed-and-left-open` before the file is touched, `filed-over-own-diff`
after — exactly one finding either side. That is the partition on one
record, which is a better statement of the property than the silence it
replaces.

3753/3753 green.

Refs: CLOUD-1311
The branching model is one commit one issue, one branch as many issues as
the work needs, one PR everything done with no punts. Nothing in the
instruction surface said so, and the one sentence that came near it said
the opposite: `.claude/rules/toolchain.md` explained the branch-keyed
claim receipt as attesting "a decision about an ISSUE that every commit on
the branch continues to serve" — singular.

The mechanism agreed with the wrong prose. `mint` takes a SLICE and line 1
of the receipt has always been an id LIST, so the many-row shape was
expressible in one invocation — but the write is `fs::write`, so a second
`claim check` INVOCATION replaced the first row's claim and said nothing.

Measured 2026-09-01: reading that sentence, an agent declined to pull a
second row onto an open branch and reported the storage key as the rule.

So the ids union, guarded by the recorded base. A restarted branch
(`git checkout -B <name> origin/main`) keeps the receipt because the file
is keyed by NAME, and carrying ids across a changed base is exactly the
stale claim CLOUD-516 measured sitting through four unrelated stories — so
a changed or unresolvable base starts a fresh list. Could-not-look drops
the list rather than carrying it: a lost claim costs one re-run, a carried
stale one is the defect.

Four cases, over the real `mint`: the join, no duplicate on re-claim, a
restart resetting, and an unresolvable base carrying nothing either way.

AGENTS.md now states the model where it binds every turn, with both
failure modes named — a branch per row is the batching `land`'s lap loop
exists to prevent, and stopping at one row is the punt
`filed-and-left-open` prices.

Refs: CLOUD-472

Admits: 4265150829ab6e4bb6ac8d3976cda321d94ca094835872fd4dceb6759672c6c1
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: be5a33c
Admits-epoch: 781d35649dedb150ccb69e890c6aa482c7393233f5ffe1dce100609afd6150fb
Admits-author: alec@wenzowski.com
Admits-prev: bc0321f2fc7cbb652a826c1d4f8928972e3d7c212a817ed94c9af37e90c74973
Admits-answer-lost: CLOUD-1311's arm is unloadable and therefore unlandable. The module edit already made is dead code until this row exists, so declining here leaves the tree strictly worse than not starting: a module that fails to load disarms `filed-unrefined` and `filed-over-own-diff` too.
Admits-answer-precondition: A `[[verdict]]` row is only expressible in batten.toml: the registry IS the surface, and `policy/filed-here.rego`'s new arm cannot load at all until `V-FILED-AND-LEFT-OPEN` is declared there — a module raising a token no row declares is refused at load. So the write to the authority is not merely the shortest route, it is the only one, and it lands in the reviewed PR for CLOUD-1311 where `mise run config-lint` and `mise run batten-check` judge it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a verdict class — there is no verb that registers one, and the path's own redirect says to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed: this adds one `[[verdict]]` row with its routes and touches no existing row. The write is additive and strictly raise-only — a new deny class with a declared override precondition — so it cannot weaken any gate, which is the property house-style §8 asks of a config change and the one a reviewer should check in the diff.

Admits: 5e94b114846dc2b86da06b29535aa4639351988b9ccd597ade5e803363af7ef2
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 8cc3fed
Admits-epoch: 78cca6d566d252da1ec49eae537f22670ac4ff47cb79a3a4d52e2451ff71c412
Admits-author: alec@wenzowski.com
Admits-prev: 4265150829ab6e4bb6ac8d3976cda321d94ca094835872fd4dceb6759672c6c1
Admits-answer-lost: CLOUD-472's ratchet cannot load, and `ready lint` is left refusing every payload on an unresolvable grammar token. The escape stays open: the claims object that CLOUD-453 built and CLOUD-418 gave its `mutation` field remains opt-in, so a §7 naming three obligations in prose keeps linting green — measured 2026-09-01 on CLOUD-1306 and on CLOUD-1311's own block.
Admits-answer-precondition: A `[[pattern]]` row is only expressible in batten.toml — the registry IS the surface, and `Grammar::assemble` resolves `ready-prose-dialect-exempt` by id with a LOUD failure, so `batten ready lint` cannot run at all until the row exists. The threshold is a consumer fact about this repository's own key space (non-negotiable rule 1), so it could not live in the crate even if there were a route. It lands in the reviewed PR for CLOUD-472 where `mise run config-lint` and the compiled tier in crates/batten/tests/it/ready.rs judge it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a `[[pattern]]` row; no verb registers one, and the path's own redirect says to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed — this adds one pattern row and touches no existing one. The row is deliberately set ABOVE every key that exists today, so it refuses nothing currently on the board and cannot darken the ready frontier the way CLOUD-858 measured; that is the property a reviewer should check in the diff, since a threshold set too low is the one way this change does harm.

Admits: 051b1f7234d5470e5dc0cfa2d30dcc6d57924594018ed39c45f34a1fe972f4ec
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/workflow/board-states.md
Admits-head: 8cc3fed
Admits-epoch: 08df746a010060ba1c781d4de6935d9751f3cf01839b0aa0ca5c09e4982d7775
Admits-author: alec@wenzowski.com
Admits-prev: 14eb1973fd24f18fe1092e42014b0cac84beef053e935f202b0aab519cb80b01
Admits-answer-lost: The correction stays a one-line rule with no record of why it was needed. Both measured failures — the singular sentence in `.claude/rules/toolchain.md` that pointed the wrong way, and `claim::mint` silently replacing the previous row's claim — would be undocumented, so the next reader re-derives the wrong model from the same two sources that produced it this time.
Admits-answer-precondition: The memory IS the owning surface for this content: AGENTS.md is at its `policy-budget` ceiling (measured this commit at 3618 tokens of 3500, which refused the first attempt), so the rationale for the branching model cannot live there and the repo's own split puts on-demand content in `.serena/memories/`. The write was made through `mcp__serena__edit_memory`, the route the redirect names, so this records the change rather than authorising a route around it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is what was taken, not rejected: the edit went through Serena's `edit_memory`, and this admission exists because `commit check` records every protected path in the diff regardless of the route that wrote it. R-RESTORE-IT does not apply because nothing was destroyed — the edit inserts two paragraphs ahead of the `claim-check` ordering section and changes no existing sentence.
CLOUD-453 built a fenced claims object whose `REQUIRED_CLAIMS` forces
`tests`, and CLOUD-418 gave every entry a `mutation` field — an obligation
that cannot name the mutation which would kill it cannot be written. That
mechanism landed and was UNREACHABLE: `check_claims` returns false on an
absent fence and the caller drops to the prose path, so the whole thing
was opt-in.

Measured 2026-09-01 over the 50-row Todo queue: nothing used it. CLOUD-1306,
filed that day, carries a §7 naming three obligations in prose, none
joinable to anything. `ready.rs` has emitted `dialect prose` per run all
along, with a comment saying the corpus would "converge deliberately
instead of in one sweep" — the sensor was right there and nothing
ratcheted on it.

A RATCHET, NOT A FLIP. `graph-check` enforces `Todo => ready-lint exits 0`,
so refusing every prose block at once takes the whole ready frontier dark
in one step, which is CLOUD-858's measured shape.

TWO WRONG SURFACES PRECEDED THE RIGHT ONE, and both are recorded where the
next author will look. First a `[[pattern]]` row spelling the exempt range
as a regex over the key: the registry gives one concept one spelling and
arithmetic is not a concept, and it decides on key TEXT, which this
consumer already declares `ready-issue-mention-markup` for because the
tracker rewrites a bare key into `<issue ...>` markup. Then a key ORDINAL,
which reaches no consumer literal and still requires keys that are numeric
AND monotonic with creation order — true of three popular trackers, false
of a slug- or UUID-keyed one, where it would resolve to nothing and fail
SILENTLY.

A creation instant assumes nothing. Every tracker stamps one, the payload
already carried it, and `policy/filed-here.rego`'s `predates_the_branch`
already compares tracker timestamps this way: both sides fixed-width
ISO-8601 UTC, so lexical order is chronological order.

Three gates caught the plumbing rather than me. `resolve` refuses a field
with no declared provenance layer; `trust` refuses a config field that does
not say what weakening means for it; `policy-budget` refused the first
attempt to state the branching model in AGENTS.md at its own ceiling. The
weakening kind is `ready-cutover-relaxed` — later exempts more rows, and
dropping the key is that move taken to its limit, since absent reads as
could-not-look.

Also here, from the same review: `.claude/rules/toolchain.md` states the
retirement of by-path hook registration as a DIRECTION rather than a
one-off, because a capability declared under a harness's own directory
exists for one of five wired harnesses and is invisible to the other four;
and `.claude/rules/policy-modules.md` records both wrong surfaces above as
a rule about never building a predicate on text a round trip rewrites.

3762/3762 green.

Refs: CLOUD-472

Admits: 62e398b8a6bd7a69ad128dc0dbc985bd085f8b36c29b27adf10386a855195e87
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: e1b0428
Admits-epoch: 08df746a010060ba1c781d4de6935d9751f3cf01839b0aa0ca5c09e4982d7775
Admits-author: alec@wenzowski.com
Admits-prev: 5e94b114846dc2b86da06b29535aa4639351988b9ccd597ade5e803363af7ef2
Admits-answer-lost: The threshold stays spelled as `^CLOUD-([0-9]{1,3}|1[0-3][0-9]{2})$`, which is the wrong surface on two counts the repository already knows about: the pattern registry exists so one CONCEPT has one spelling and arithmetic is not a concept, and the decision would turn on key TEXT that the tracker demonstrably rewrites — this file declares `ready-issue-mention-markup` precisely because a bare key returns wrapped in `<issue …>` markup.
Admits-answer-precondition: Both halves of this edit are only expressible in batten.toml: it removes the `[[pattern]] ready-prose-dialect-exempt` row and adds the `[ready]` table that replaces it, and neither a pattern row nor a config table has any owning verb — the file IS the surface. `Grammar::assemble` resolves pattern ids with a LOUD failure, so leaving the stale row while the code no longer reads it would be dead config, and removing it without adding `[ready]` leaves the ratchet unreachable. It lands in the reviewed PR for CLOUD-472 where `mise run config-lint` and the compiled tier judge it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for both a `[[pattern]]` row and a config table; no verb registers either, and the path's own redirect says to change it in a pull request. R-RESTORE-IT does not apply in the usual sense but half of this edit IS a restore: it removes a row added earlier in this same branch rather than one that ever landed, so the net effect against origin/main is one new `[ready]` table. The threshold is set above every key that exists today, so nothing on the board is refused and the ready frontier cannot go dark the way CLOUD-858 measured — that is the property to check in the diff, since a threshold set too low is the one way this does harm.

Admits: e21549fcda550f97b7487fa035daa0f02606801a168f44f2f5b8ec7c0cc74feb
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: e1b0428
Admits-epoch: 3a81b526ea895c28ff9a18819a9251bedd1f695f8c1146f88ca33f86464cdddb
Admits-author: alec@wenzowski.com
Admits-prev: 62e398b8a6bd7a69ad128dc0dbc985bd085f8b36c29b27adf10386a855195e87
Admits-answer-lost: The prose-dialect ratchet cannot be reached at all. `Grammar::with_prose_threshold` receives `None`, which is could-not-look by design, so `ready lint` exempts every row and the claims object stays opt-in — the exact state measured on 2026-09-01, where the mechanism CLOUD-453 built and CLOUD-418 gave its `mutation` field was used by nothing across the whole 50-row Todo queue.
Admits-answer-precondition: This is the second half of one replacement and the file IS the surface for it: the `[[pattern]]` row was removed under the previous admission, and `[ready] prose_dialect_exempt_below` is what the code now reads. No verb registers a config table, and leaving the tree between the two writes is strictly worse than either end state — the ratchet would be unreachable while `config.ready` is `None`, which reads as could-not-look and exempts every row. Split across two admissions only because each covers one write; it should have been one edit, and that is my error rather than a property of the change.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a config table; there is no verb that writes one, and the path's own redirect says to change it in a pull request, which is what this is. R-RESTORE-IT does not apply because nothing was destroyed — this adds one table and touches no existing one. The value is set above every key that exists today (highest live row CLOUD-1311), so nothing on the board is refused; a reviewer should check that number specifically, because setting it too low is the one way this change takes the ready frontier dark the way CLOUD-858 measured.

Admits: 8164d00fb3d38d7017b27ed86e88026385f9529023dee50bf1b1ffa6afb28753
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/workflow/agent-fanout.md
Admits-head: e1b0428
Admits-epoch: 04d65e929fcb8962b43da408ccb71d1f8d5a63c839a9492142bf7009f372e12a
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Three corrections stay unrecorded and get re-derived wrongly, as they were this session. The WIP cap keeps reading as a bound on tickets rather than on builds, so an agent splits work that belongs in one PR. Bundling keeps being justified by rebase amortisation, which the same file's caps section already refutes — a rebase costs no model turn, so there is nothing to amortise — and that argument gets weaker as automation improves, which is the tell it was never the reason. And nothing records that this repository caps no PR size, so an invented threshold recurs.
Admits-answer-precondition: The memory IS the owning surface: AGENTS.md is at its `policy-budget` ceiling (3618 of 3500 tokens, measured when it refused this session's first attempt), so fan-out rationale cannot live there, and the repo's own split puts on-demand content in `.serena/memories/`. The write was made through `mcp__serena__edit_memory`, the route the redirect names; this records the change rather than authorising a route around it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is what was taken, not rejected: the edit went through Serena's `edit_memory`, and this admission exists because `commit check` records every protected path in the diff regardless of the route that wrote it. R-RESTORE-IT does not apply because nothing was destroyed — the edits replace two wrong rationales with the measured ones and add the maximise-the-bundle direction; every existing measurement, including the owner-set caps of 6 and 2, is left exactly as it stands.
The harness task list is the most legible statement an agent makes about
what it believes is outstanding, it is displayed every turn, and nothing
in the tree could see it. So a branch landed with half its list `pending`
and every gate stayed green.

That is this repository's recurring defect rather than a new one — a
sensor with no ratchet on it. `stop_nudges` rule 5 asks the right question
with no exit code; `ready.rs` emits `dialect prose` with nothing
downstream; `graph-check` counts a `wip` in the wrong unit. Measured
2026-09-01: three items sat `pending` while the session reported the work
as planned, and the only detector was a human asking.

A VERB WRITES THE STORE, NOT A HOOK, and that is the design rather than a
convenience. Recording from the harness's own todo tool needs a spelling
per host — `TaskCreate`/`TaskUpdate` here, `write_todos` on Gemini CLI,
`todowrite` on OpenCode, `update_plan` on Codex — and fails three
different ways that are indistinguishable at the gate: an unsurveyed
harness, a tool a setting switched off, and a compliant agent all record
nothing. OpenCode makes it concrete by denying `todowrite` to subagents at
session creation whatever the config says. `batten record plan` inverts
the direction: the agent tells the engine, so a missing record REFUSES,
identically everywhere, with no survey and no setting that can disarm it.

Two arms, and the second is not optional. `plan-unfinished` refuses an
entry left in flight. `plan-unrecorded` refuses a branch that recorded
nothing at all — without it the first arm is satisfied completely by
silence, which is the vacuity `mutate` already refuses by REPORTING a
declared mutation whose named case does not exist rather than counting it.

THE VACUITY ARM'S FIRST DRAFT WAS TOO WIDE, and the measurement is why it
is keyed on the claim receipt now. Asking only for a non-empty diff is
true of every scratch fixture and every consumer checkout: it reddened
four `cli.rs` cases whose only business was exercising unrelated rules. A
rule that fires on any dirty tree makes the committed config unusable over
a test repository, and a rule like that gets switched off.

`input.tree.records` already reads `.git/batten-receipts/<name>.<branch>`,
so no new `Fact` was needed — but `recorder_records` read only the
DECLARED stores, so a verb-written one was invisible whatever a module
asked for. It now unions `record::VERB_WRITTEN`, unconditionally: the
engine owns both ends, so there is no declaration for a consumer to forget
and no unrelated table to make a gate's liveness depend on.

Five gates caught halves I would otherwise have shipped: the leaf-verb
dispatch census, the emitted row set, the mutation census, the pointer-only
disposition, and the derived man/completions artifacts.

3773/3773 green.

Refs: CLOUD-472

Admits: aba1e62b2c5357a2f1c428f8043c4360b5eb3c467e39a3b88b34237f6b474abb
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 3c63c4e
Admits-epoch: 04d65e929fcb8962b43da408ccb71d1f8d5a63c839a9492142bf7009f372e12a
Admits-author: alec@wenzowski.com
Admits-prev: e21549fcda550f97b7487fa035daa0f02606801a168f44f2f5b8ec7c0cc74feb
Admits-answer-lost: The plan store has a writer and no reader, which is exactly the dead-gate class `crates/batten/src/record.rs` exists because of — its header records two landed readers with no writer, two `deny` rows deciding nothing. Here it would be the mirror: `batten record plan` writes a store no rule reads, so an agent could land with half its declared work in flight and every gate stays green, which is the defect measured on this very session.
Admits-answer-precondition: A `[[rule]]` row and its `[[verdict]]` classes are only expressible in batten.toml — the file IS the registry, no verb writes either, and `policy/plan-complete.rego` cannot load at all until `V-PLAN-UNFINISHED` and `V-PLAN-UNRECORDED` are declared, because a module raising a token no row declares is refused at load. The path's own redirect says to change it in a pull request, which is what this is, and `mise run config-lint`, `mise run policy-test` and the compiled tier judge it before it binds.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a rule row and a verdict class; there is no verb that registers either. R-RESTORE-IT does not apply because nothing was destroyed — this adds one `[[rule]]` row and two `[[verdict]]` classes with their routes, and touches no existing row. The addition is strictly raise-only: two new deny classes, each with a declared override precondition, so it cannot weaken any gate. What a reviewer should check is the `plan-unrecorded` arm, because it refuses a branch that recorded nothing and therefore has the widest blast radius of anything in this change; it is gated on a non-empty diff and satisfied by an empty record, so the remedy is one call rather than a fabricated entry.

Admits: 8137dd28e8352d32c3b5fd36e7a5f7152b979223890f2f11c5add2c1528c16ee
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 3c63c4e
Admits-epoch: 1893a9cd9dbd090116123001501b72aa60042d6906885362d4b43c0f6cfad073
Admits-author: alec@wenzowski.com
Admits-prev: aba1e62b2c5357a2f1c428f8043c4360b5eb3c467e39a3b88b34237f6b474abb
Admits-answer-lost: The rule row cannot load, and a module that fails to load takes its whole bundle with it. The plan store keeps a writer and no reader — the dead-gate class `record.rs` was written because of, in mirror image.
Admits-answer-precondition: Second half of one change, split only because an admission covers one write. The `[[rule]]` row landed under the previous admission; `policy/plan-complete.rego` raises `V-PLAN-UNFINISHED` and `V-PLAN-UNRECORDED`, and a module raising a token no `[[verdict]]` row declares is refused at LOAD — so the tree between the two writes does not merely lack a feature, the module fails to load. batten.toml is the registry and no verb writes a verdict class.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for a verdict class, and the path's redirect says to change it in a pull request. R-RESTORE-IT does not apply because nothing was destroyed — two classes and their routes are added, no existing row is touched, and the change is raise-only. The route to check in review is `R-OVERRIDE-PLAN-UNFINISHED`'s precondition: it must be answerable only for work that genuinely is not this branch's to finish, or the gate becomes payable in typing, which is the failure `V-FILED-UNREFINED` measured when a Ready block turned out to be the cheapest thing an agent can produce.

Admits: 1e57c511055e0626046bd28c09c30389ad540aea48e85b0abecdf534385b9966
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/plan-complete.rego
Admits-head: 3c63c4e
Admits-epoch: 02358ef15596a3d6bbc0781748ec5d6637cad09abc0c7e143f2eebeb2770950d
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The arm stays too wide and the committed config becomes unusable over any scratch tree. Measured just now: `plan-unrecorded` keyed only on a non-empty diff reddened four `cli.rs` cases — `the_committed_delegating_rule_spawns_nothing_when_its_glob_misses`, `a_tracked_instruction_may_not_prescribe_the_denied_commit_identity`, `the_committed_portability_rules_fire_on_every_banned_shape` and one more — whose only business was exercising unrelated rules over a fixture repository. A rule that fires on every dirty tree is one that gets switched off.
Admits-answer-precondition: A registered .rego module has no owning verb — the file IS the surface that declares the predicate, so narrowing `plan-unrecorded`'s precondition can only be done by writing it. The write lands in the reviewed PR for CLOUD-472, where `mise run policy-test`, the compiled tier in crates/batten/tests/it/plan_complete.rs and the declared `#MUTANT` rows judge it before it binds anything.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because no verb writes a predicate into a registered module; the module file is the owning surface and the path's redirect says to change it in a pull request. R-RESTORE-IT does not apply because nothing is being restored — this NARROWS a refusal I added minutes ago in this same branch, which is a tightening of the change rather than a reversal of anything landed. The property to check in review is that the new precondition is the claim receipt: it must select branches doing tracked work and nothing else, because keying it any wider is what the measurement above refutes and keying it narrower would let an unclaimed branch escape the gate entirely.
`unfinished-entry-unread` mutated `not done(entry.status)` while the arm
binds `entry_row`, so the sed matched nothing and `mutate sweep` reported
`inert-mutation` rather than counting it.

INERT AND SURVIVED ARE DIFFERENT DEFECTS, which is why the runner keeps
them apart: a survivor altered reachable code and its named case could not
observe the change, while an inert row never altered anything at all. Read
as coverage, both are the same lie — the census counts the module enforced
while the sweep has proved only that a no-op leaves the suite green, which
is CLOUD-418's finding reproduced inside the mechanism built to catch it.

Sweep after: `plan-complete` reports neither row, and the totals are back
to the 13 survivors and 1 could-not-look that stood on `main` before this
branch.

Refs: CLOUD-472

Admits: f67a29e4dfe3e0b673c8ee323b505f75b6723efc16306b55c9b55899715b7dfc
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/plan-complete.rego
Admits-head: dda2bdb
Admits-epoch: 02358ef15596a3d6bbc0781748ec5d6637cad09abc0c7e143f2eebeb2770950d
Admits-author: alec@wenzowski.com
Admits-prev: 1e57c511055e0626046bd28c09c30389ad540aea48e85b0abecdf534385b9966
Admits-answer-lost: `plan-unfinished` ships with a declared mutation that does nothing, which is worse than declaring none: the census counts the module as enforced while the sweep proves only that a no-op leaves the suite green. That is exactly CLOUD-418's finding — a test that cannot discriminate shipping as coverage — reproduced inside the mechanism built to catch it.
Admits-answer-precondition: A `#MUTANT` row lives in the module beside the predicate it mutates — there is no other surface that can carry it, and a registered module has no owning verb. The row is currently INERT: its expression names `entry.status` while the arm binds `entry_row.status`, so the sed matches nothing and `mutate sweep` reports it rather than counting it. Fixing it means writing the file. It lands in the reviewed PR for CLOUD-472 where the sweep itself is the check.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because no verb writes a `#MUTANT` row; the module is the surface. R-RESTORE-IT does not apply because nothing was destroyed — this corrects an expression added in this same branch so that it actually alters reachable code. What a reviewer should check is the sweep output rather than the diff: the row must move from `inert-mutation` to caught, because a mutation that applies and is still not observed by its named case is a different defect wearing the same word.
…d none

`operation_of` is a static table from one survey (CLOUD-209), and its own
comment records the trap: Gemini and Copilot carry no spellings because
the survey did not record them, which is an absence of DATA that reads as
an absence of CAPABILITY. I made exactly that inference earlier in this
branch — treating a config gap as proof those hosts had no todo tool, and
designing around an abstention that was never established.

So the capability matrix gains a column, and it has two variants for a
three-valued fact. `Surveyed(&[])` is a measured "this host offers none";
`Surveyed(&[..])` carries spellings that were FETCHED; `Unsurveyed(owner)`
is nobody having looked, and is never reported as none.

Fetched 2026-09-01 from vendor docs: Gemini CLI `write_todos` (on by
default, disableable via `useWriteTodos`), Codex CLI `update_plan`, and
this host's own `TaskCreate`/`TaskUpdate`. Cursor was SEARCHED and is
still `Unsurveyed`: it has a Todos feature since 1.2, but the results were
forum threads and third-party articles rather than a documented tool name,
and a spelling taken from those is what CLOUD-209's rule refuses.

BATTEN DOES NOT GATE ON THIS COLUMN. `plan-complete` reads a store
`batten record plan` writes, so it fails closed on every host whatever the
table says. What the column buys is the mirror — keeping the human's
native todo view in step — and an honest report of hosts where that view
does not exist.

`doctor` reports it, and the check's shape is the interesting half. A
check that reddened on every unsurveyed host would be permanently red
here, and a diagnostic that never goes green stops being run. So an
unsurveyed row must NAME the row that owes the survey, and naming one
changes no exit code — `#MUTANT-OWNER`'s bargain one layer over, where the
declaration buys that the gap is stated rather than that it is forgiven.
What it catches is a harness added with neither a fetch nor an owner,
which is the moment the gap goes invisible.

3775/3775 green.

Refs: CLOUD-472
`REQUIRED_CLAIMS` has forced `tests` since CLOUD-453 and every entry has
carried a `mutation` since CLOUD-418 — the obligation as a field, where an
entry that cannot name the change which would kill it cannot be written.

It was PROSE, so it was joinable to nothing. A slug is: `batten mutate`
resolves it, applies the expression, runs the named case, and a SURVIVOR
is the finding. That is the difference between "pressure tested" as an
assertion and as an exit code.

SHAPE HERE, RESOLUTION AT `verify`. The case does not exist at refinement
time, so resolving the slug now would refuse every honest row before its
code was written — the false-failure trap CLOUD-472's own §3 names.
Whitespace is the whole discriminator, because `mutate`'s three-field row
format already forbids it in a slug.

The shared fixture carried the exact defect the check exists for — a
sentence where a resolvable token belongs — so it is corrected at source
and every case inherits the right shape. Both directions are asserted:
prose refused, and the unmodified fixture clean, because a refusal whose
remedy is unreachable is a wall.

Also fixes a duplicated `#[test]` this branch introduced, which the test
tier could not see: it is a warning, and only clippy's `-D warnings`
promotes it.

3775/3775 green.

Refs: CLOUD-472
…-first

CLOUD-761 measured twenty independent derivations of the key pattern in
nine spellings, diverged on three axes, with a shipped defect behind
them: a body writing the lowercase form is accepted by one gate and
invisible to two others. This is the definition those sites converge on,
plus the gate that stops a twenty-first arriving.

THE CRATE OWNS THE AXES AND NEVER THE TOKEN. `no-tracker-key-in-core`
refuses a derivation anywhere under `crates/**` — the mechanism that
exists because CLOUD-1121 carried the literal in as a `const` and passed
every gate — so the token stays the consumer's, read from the
`[[pattern]]` registry, and `ready::Grammar` owns case, boundary and
anchoring. The tests spell the prefix from parts for the same reason: a
test is not exempt from the rule it is testing.

THREE OPERATIONS, AND SEPARATING THEM REMOVES A DERIVATION RATHER THAN
ADDING ONE. `key_of` answers "is this whole string a key" — the four
shell `case` globs' question, which they get wrong because a glob cannot
anchor, accepting `AB-1`, `Z-9`, `A-1foo` and a key with a trailing
letter. `keys_in` answers "which keys does this text carry". The third
question — "does this text carry key K" — needs no expression at all: it
is `keys_in` compared for equality, so the boundary the two landed sites
commented on is decided once.

The boundary is read off the bytes either side of a match rather than
composed as `(^|[^0-9A-Za-z-])…([^0-9]|$)`. `regex` has no lookahead, so
a trailing class would CONSUME the byte after a match and make adjacent
keys unfindable — and checking bytes means the crate composes no key
expression, so there is nothing here for a twenty-first copy to be a copy
of.

THE GATE RATCHETS RATHER THAN FORBIDS, which is what makes it landable.
34 occurrences survive across the task tree; a `forbid` fires on every
one. Converting them means editing governed programs, which is CLOUD-761's
remaining half. Meanwhile the count cannot grow.

Replayed over 400 commits of origin/main as CLOUD-1142's §7 requires,
before the severity was set: 400 examined, 1 would-fire, 0 false
positives. The one firing is `c64e54a3`, a revert that put four
derivations back — a true positive, and the same false premise this row's
own blocker rests on.

Shown able to fail, and it caught a dead gate doing it: the first draft
wrote the pattern in escaped regex form, but `ratchet_rule` counts with
`str::matches` — a literal substring — so it counted zero at both ends and
could never fire. Driving a twenty-first derivation through it is what
found that; reading it did not.

Admits: f3cdc5c1a49523305bc7451d2cc9bdd05a313d96a00e21164b405c6ce170bdc2
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: 9ece058
Admits-epoch: 7dcd0bd54e1da3834c9350ebcdeb40e91dbeb1c8a268d6466d66a26b459753c2
Admits-author: alec@wenzowski.com
Admits-prev: b28fd480c8126febacd3db96118787d32ebab604e9dce98f869210441887098e
Admits-answer-lost: The property whose absence is how twenty derivations arrived. CLOUD-761 measured twenty independent spellings of the issue-key pattern diverged on three axes, with a shipped defect behind them — a body writing the lowercase form is accepted by one gate and invisible to two others. Without this row the count can keep growing and nothing notices, which is exactly the history: replayed over 400 commits of `origin/main` the predicate fires once, on `c64e54a3`, a revert that put four derivations back. That firing is a true positive and there are no false ones.
Admits-answer-precondition: The change adds one `[[rule]]` row, `issue-key-derivations-not-growing`. A rule row IS the committed authority — no verb writes one, and CLOUD-1142's §1 names this file explicitly as where the anti-duplication gate's identity, applicability, scope and severity are declared. It lands beside `no-tracker-key-in-core`, the row that already refuses the same derivation under `crates/**`, so a reviewer reads the two halves of one predicate together: forbidden in the crate, ratcheted in the task tree.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because this file is the owning surface for a `[[rule]]` row and CLOUD-1142's §1 says so by name. R-RESTORE-IT is rejected because restoring the committed bytes restores the ungated state, which is the defect rather than a fault to undo.

Refs: CLOUD-1142, CLOUD-761, CLOUD-1121
The id union landed without its other half. `config lint`'s groomed reader
resolves `weakens <id> <smell> <key>` out of the claim receipt (CLOUD-841),
and the loop that writes those lines walks the issues of THIS invocation —
so claiming a second row carried row one's id forward and dropped row one's
admission, and the refusal that follows names a smell whose groom sits in
the tracker where no gate reads it. That is CLOUD-1231's third acceptance
clause, which the ids alone do not meet.

Both carries are now guarded by one predicate rather than two spellings of
it, so the restart case cannot forget a claim and keep an admission.

A row named in this invocation is deliberately NOT carried: the payload in
hand is the authority for it, so a clause groomed OFF a row disappears
rather than surviving in a file nobody re-reads. Without that the carry is
a ratchet only a hand-edit can lower.

Three cases, and the positive one was shown to redden with the carry
stubbed out; the two negatives assert absence and stay green under it,
which is what says they are testing the guard rather than the write.

`plan` joins the verdict vocabulary in the same commit, since
`verdict_vocabulary.rs` holds the declared table and the measured candidate
list to each other in both directions.

Refs: CLOUD-1231, CLOUD-841, CLOUD-516
@wenzowski wenzowski changed the title refactor(ci): retire bot-issue onto the engine, and close every row it surfaced refactor(ci): retire bot-issue onto the engine, and close every row this session opened Sep 2, 2026
CLOUD-472's core. `ready-lint` gates the SHAPE of a Ready block and `verify`
gates the CODE, and nothing compared them — so an obligation could be written
into an issue, pass the refinement gate, and land with no test behind it, with
every gate in the loop green while it happened. CLOUD-369 is the measured
instance: an acceptance bullet describing when a second matrix may be bought did
not describe the code that buys it, and it merged by fast-forward, CI-confirmed
green.

The reconstruction is what makes it structural rather than inattention. The
acceptance was written first and correctly; a collision forced a
mid-implementation redesign; the redesign was derived from the problem again
rather than from the acceptance and silently dropped one condition; and THE
TESTS WERE WRITTEN FROM THE IMPLEMENTATION, so they assert what the code does.
Tests written that way can only ever confirm it — they are structurally
incapable of catching a dropped obligation, because a missing behaviour has no
code to write a test against. The Ready block is the only artifact that still
remembers what was promised, and nothing read it at implementation time.

`ready lint` now emits the declared `<file>:<slug>` set before any verdict, for
the same reason `cites-body` goes out early: emitting it after a refusal makes
the set unavailable for precisely the rows most likely to carry a broken one,
and a consumer reads that absence as could-not-look over a block that was read
perfectly well. A recorder column carries it into the tree over the description
the TRACKER returned — the forgery control the `verdict` column already has,
earned when `ready-lint` over a self-assembled payload was measured green three
times against text in a local file, once under an id no row carried.

THE COLUMN IS ON ALL THREE `board-writes` ROWS, because the readers index
positionally: a recorder that skipped it would put its neighbour's value where
this one belongs for exactly the rows it did write.

`policy/obligations-bound.rego` then refuses an obligation whose file this
repository does not track, and — separately, because the remedies differ — one
whose file exists and whose slug no `#MUTANT` row in it declares. This rule and
`mise run mutant` are one obligation in two halves: this asks whether something
is bound to the promise, the sweep asks whether it discriminates, and a case
that cannot fail is not coverage (CLOUD-418).

Three-valued throughout. `-` is could-not-look and passes, because a
prose-dialect block emits no line and reading that as "declares none" would
exempt exactly the rows this gate exists for. `0` is a measured zero and passes.
An absent record is silent.

`.claude/rules/toolchain.md` gains the wider half of the harness-directory rule:
the SUBJECT decides between a hook and a verb. A hook mediates a call to
somebody else's tool, so where the host spells it differently, switches it off,
or was never surveyed, the envelope never arrives and the gate passes silently;
a verb's subject is a record the agent minted, so a missing one refuses
identically everywhere. The three measured instances are recorded, and so is the
correction that disableability is universal and is NOT the argument — the
argument is that absence must be a reading, not silence.

Verification: `policy test` 45 bundles / 551 passed; `config lint` 0 smells;
`test:cargo` 3965/3965, the six new cases included.

Refs: CLOUD-418, CLOUD-369, CLOUD-209

Admits: 107a67c2786f9529a039cdf38a7e9507320ba01e1824f345ae455f18f8329e9f
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: b45e374
Admits-epoch: 34bb6def18926115877b0a377a8053da893a5a6f3899b3870e4a73bf29bcee50
Admits-author: alec@wenzowski.com
Admits-prev: 256844626f24d9ecda229d209b816e36e927c5f14c8b3e9b49f3ded022d895aa
Admits-answer-lost: `policy/obligations-bound.rego` becomes a module nothing runs: no rule row means the engine never loads it, no verdict row means its token fails registry validation, and no recorder column means the obligation set never reaches the tree. CLOUD-472 would land as three files that decide nothing — the dead-gate shape this change exists to remove.
Admits-answer-precondition: A `[[rule]]` row, a `[[verdict]]` row with its four routes, and a `[[recorder.columns]]` entry are all consumer policy, and `batten.toml` is the one committed authority that declares them — there is no other surface that can express a new gate, so writing it directly is the only route. The whole write is additive and lands in this diff where a reviewer reads it beside the module it declares.
Admits-answer-rejected-route: `config read first` is the one that names this case and it does not apply: reading the config is how the row shapes above were copied from `plan-complete` and `issue file held`, and reading it again changes nothing about the fact that the declaration must be WRITTEN there. `patch run first` rejected too — there is no generated or derived source for this content, so a patch would be the same bytes through another door.
`filed-over-own-diff` exempts a row the pull request CLOSES and reads that
from `input.tree.records["pr-closes"]`. Until now that record had one
producer: the `pr-body-closes` `[[recorder]]` row, minted from an observed
`gh pr view --jq .body` tool envelope. So the exemption was reachable only
when an agent happened to make that call, as a mediated tool call, on a
harness whose spelling is surveyed.

MEASURED ON THIS BRANCH, which is what turns a design smell into a defect:
three rows it closes in its own body, all three refused, and no `pr-closes`
record in the store at all — while `land` had fetched exactly that body and
piped it to `filed-here-check`, whose task body is `batten check`, declared
`read`, with no stdin channel. Fetched, handed over, dropped. The gate was
pricing a punt that was not there and the author had no route to say so.

`record plan`'s argument, one layer over: a verb inverts the direction, so
the record exists identically on every harness and a missing one refuses
rather than reading clean.

`keys_closed_in` FILTERS `keys_in` rather than searching again, so this is a
consumer of CLOUD-1142's one definition rather than a twenty-first
derivation — and the closing verbs are the forge's vocabulary in a
`[[pattern]]` row, anchored at the END so the token immediately before a key
decides it. Naming a row is not closing one (CLOUD-674), and the case that
pins it is the one that would otherwise exempt every row a body mentions.

Zero is a count: `closes 0` says the body was read and closes nothing, an
absent record says nobody looked, and an EMPTY body refuses rather than
recording the first — a fetch that failed must not become a measurement.

Five cases over the compiled binary, reading the record back through the
engine's own `record_path` so a naming change reddens here rather than
pointing the reader and the writer at different files.

Refs: CLOUD-1311, CLOUD-1051, CLOUD-1142, CLOUD-674, CLOUD-514

Admits: 9d6eca64a31cb3a1ace83c435907d74675e90fae08b8af79e0db4790ce7b6979
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: b45e374
Admits-epoch: e214c143761fa98c543311176bd57eba127b3fcc3f7ad6697c97b5f582a3ca22
Admits-author: alec@wenzowski.com
Admits-prev: f3cdc5c1a49523305bc7451d2cc9bdd05a313d96a00e21164b405c6ce170bdc2
Admits-answer-lost: `filed-over-own-diff`'s exemption for a row the pull request closes stays unreachable in the ordinary case, which is not a neutral gap: measured on this branch, three rows it closes were refused with no `pr-closes` record in the store at all, while `land` had already fetched that body and dropped it. The gate then prices a punt that is not there, and the author's only exits are an admission asserting a precondition that is false or a hand-written record, which is worse than the defect.
Admits-answer-precondition: A `[[pattern]]` row is only expressible in batten.toml: the registry IS the surface, `Grammar::assemble` resolves `ready-closing-verb` by id with a loud failure, and `.claude/rules/policy-modules.md` refuses an inline regex outright — so `batten record closes` cannot run at all until the row exists. The token is the forge's closing-verb vocabulary, a consumer fact about which forge this repository lands on, so non-negotiable rule 1 forbids it living in the crate even if there were a route. It lands in the reviewed PR for CLOUD-1311 where `mise run config-lint` and `crates/batten/tests/it/record_closes.rs` judge it.
Admits-answer-rejected-route: config read first does not apply because batten.toml IS the owning surface for a `[[pattern]]` row — no verb registers one, and the path's own redirect says to change it in a pull request, which is what this is. patch run first does not apply because nothing was destroyed: this adds one pattern row and touches no existing one, so the write is additive and strictly raise-only. It declares a new token that only ever narrows which keys read as closed; it cannot weaken any gate, which is the property house-style §8 asks of a config change and the one a reviewer should check in the diff.
…t bind

Both `#MUTANT` rows on `obligations-bound` spelled the comprehension variable
`obligation`, where each arm binds `obligation_row`. The sed matched nothing, so
neither mutation was ever applied and the sweep reported `inert-mutation` for
both — a declared obligation that had never been shown able to fail, which is
the same class the module itself exists to refuse, one level up.

Reported rather than silently counted, which is the runner working as designed:
an inert mutation is distinct from a SURVIVED one, and collapsing them would let
a row that mutates nothing read as a row nothing kills.

Second instance this session — `plan-complete`'s `unfinished-entry-unread` named
`entry.status` against an arm binding `entry_row.status`. The tell is identical
and the sweep is the only thing that sees it, because a `#MUTANT` row is prose
to every other gate.
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Contributor Author

Closing this and continuing on claude/branch-ownership-and-retirement-bundle, because two sessions were writing this branch at once and one of them is mid-port of the land loop.

What happened, since it is the reason and also a finding

At 01:14Z a sibling session pushed 81242abb onto this branch while this session was building on b45e374c. This session's push was rejected non-fast-forward — git's own check was the only thing in the entire stack that noticed, and it noticed after the work was written, verified and committed.

Nothing in Batten fired, and none of the three mechanisms that look like they should have is actually asked this question:

  • claim-not-raced asks whether a key is claimed by a different open PR. Both sessions served this one, so it is correctly silent — it is built for two branches over one issue, the mirror of this.
  • The claim receipt, the one artifact that says this session is working this branch, lives under $GIT_DIR, is never committed, and dies with the container. No clone can read another's. Every ownership fact this repository records has that shape, and the thing being contended is the remote.
  • land-lock owns exactly the right primitive — a fleet-wide server-side CAS, chosen because it works between clones that cannot see each other — but its unit is which branch may spend CI next. Two sessions on one branch are, to that lease, one branch.

What carries over

Everything. The new branch is cut from this one's tip, so all 26 commits here plus the sibling's 81242abb and c9b2e3ca come with it — those two are CLOUD-472's core (an obligation declared in a Ready block must name a real #MUTANT case) and build directly on 7a9e12f6, which this branch already carried. Reviewed and kept.

Nothing is left behind on this branch.

What is added there

A row for the destruction half, which needs no network: trunk-based/no-force-push already denies --force and -f, and deliberately excludes --force-with-lease as "the sanctioned form". That holds when the sibling's push arrived after your last fetch. It fails for the sequence an agent actually runs — git fetch moves the remote-tracking ref onto the sibling's commit, the lease then compares equal, and the push succeeds. The flag chosen for being careful is the one that destroys the commit.

The detection half — a per-branch ownership ref taken by land-lock's own CAS — needs receive-pack over the vendored client (CLOUD-1274) and is filed rather than smuggled in.


Generated by Claude Code

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