docs(agents): a board state is a claim about the tree, and the tree wins - #815
Conversation
CLOUD-1305 A board state is a claim about the tree and nothing tells an agent to check it — the reading half of CLOUD-1253, which no gate can carry
Why
It cannot carry the other half, and the other half is where the loss happens. A gate runs at Non-negotiable rule 3 settles that this half cannot be gated: "a gate resolves to a command and an exit code over an object it decides, never a model verdict." "Did the agent check the tree before believing a row" is a model verdict. So it is feedforward, in the same shape Measured, twice in one session, 2026-09-01
That second one is a sub-shape What the prose has to say, and where
Three clauses:
Refinement — Ready (feedforward prose; the gated half is CLOUD-1253's) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found when the person I was working for pointed out that I had reported a shipped-looking row as live work, and that my correction to a half-shipped row was a paragraph rather than a state change. CLOUD-1253 A retirement row reaches Done while its subject is still tracked, and nothing decides otherwise — four rows, eight governed paths and five verbs recorded as retired that are not, plus the campaign owner that closed against its own acceptance
Why AGENTS.md makes Done a release predicate — "Done = the DoR/DoD spec's Done holds — released, yours to set, never the merge ( Measured against
Nine governed paths and five verbs are counted as retired and are not — eight in the table above plus CLOUD-1232's three, added 2026-08-31; see the section below. Checkable rather than asserted: It is not a bulk-close artifact, and that is the pointThree of the four went Done inside a ~4-second cluster ( CLOUD-1110 closed three days earlier, on its own. So the failure is not one careless sweep: it is that nothing anywhere decides the question, and a row can be closed by any route — a sweep, a hand edit, a PR body's closing key — without the tree ever being consulted. A FIFTH instance, at the campaign level, that this row's own predicate would NOT catchCLOUD-1151 — the row that owns waves 2–5 — also went Todo → Done directly at
127 of 132 governed programs are still in the tree. It closed doing precisely what it was filed to prevent, leaving waves 2–5 unowned for the second time. It has been reopened to Todo carrying that evidence and the build order it owed. It is named here for the class and deliberately NOT for the predicate. CLOUD-1151 carries no THREE MORE, found 2026-08-31 while grooming the campaign's blocked column — and only ONE is this row's predicateThe split matters, because this row is deliberately narrow: its subject is a A FIFTH PATH INSTANCE — in scope, and it belongs in the table above.
Its title is "the last unpinned interpreter in the tree can only leave by retiring the* TWO MORE OF THE CLASS THAT THIS ROW'S PREDICATE CANNOT AND SHOULD NOT REACH, named
The common shape across all three, and it is the one worth naming: a row closes when Why it matters more than bookkeepingEvery count this campaign publishes inherits the error optimistically, and the campaign is steered by counts. CLOUD-1166 already measured the adjacent defect — "a count copied out of another row's body is republished as 'today' and nothing re-derives it" — and this is the same class one level up: the board is the copy, and the tree is the thing nothing re-derives it against. Two of the four also had their real blocker land in the meantime, so closing them removed genuinely reachable work from the queue. CLOUD-1160 is 135.1s, the second-largest suite in the tree and the largest single-program retirement available; its blocker CLOUD-1167 is Done. Both of CLOUD-1162's blockers (CLOUD-1168, CLOUD-1154) are Done. Those rows have been reopened with this evidence. CLOUD-940 records the neighbouring shape for the wiring surface — "a guard retired from the wiring but left in the tree is invisible to every wiring gate" — and CLOUD-908 records what a retirement owes when it does happen. Neither asks whether a CLOSED row retired anything. The predicate has a subject already, and this row must not invent a second oneThe obvious framing — "parse §1 for deleted paths" — would be a second authority over what a retirement is, and prose is not a subject a gate can decide over. The ledger already is that subject.
A row whose ledger carries no arm at all claims no retirement and is out of scope — which keeps this off every non-retirement row and stops it becoming a second DoD. Refinement — Ready (decide a closed retirement against the tree, from the ledger that already exists) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found while answering "what unblocks 100% of the bash retirement campaign": three of the classes I was about to call cleared rested on rows whose Done turned out not to mean the program was gone. CLOUD-843 The retirement campaign has no row: eight capability rows cite "so the 79 gates have somewhere to migrate onto", and nothing owns the migration — measured, the bash grew today
Why Eight rows in this campaign are justified by a migration nobody owns. CLOUD-833's title is literally "so none of the 79 gate-described mise-tasks has a surface to migrate onto"; CLOUD-832 exists so a bundle can carry 79 predicates; CLOUD-807 built the permit that lets a suite die with its subject. CLOUD-312 owns the 11 hook bodies and only those. The 82 gate-described Measured on
|
| Surface | Morning | Now | Δ |
|---|---|---|---|
mise-tasks/ files |
133 | 136 | +3 |
mise-tasks/ lines |
27,799 | 28,590 | +791 |
| gate-described tasks | 79 | 82 | +3 |
tests/*.bats lines |
34,654 | 36,052 | +1,398 |
| bats cases | 2,485 | 2,548 | +63 |
| gate tasks migrated | — | 0 | — |
kind = "policy" rows |
0 | 1 — and it is trunk-based-preset, a vendored preset, not a migrated gate |
One bash file was retired all day: .claude/hooks/batten-hook.sh, 65 lines (CLOUD-824). Against +791 lines added. The campaign to delete bash added bash, and nothing on the board was positioned to notice, because no row carries the number.
The classification, by what each task invokes
Not by substring. A string scan over these files put ci-local-parity and pipefail-grep-check in the forge bucket — the same instrument that counted 14 spawn sites where name resolution found 9 (CLOUD-743). Classified instead by the external programs each task actually invokes in command position:
| bucket | count | can it migrate? |
|---|---|---|
tree — no git, no forge, no build |
22 | yes, on CLOUD-833's tree surface, today |
| git facts | 50 | needs a git fact on the tree document; scope unproven |
| build/bench | 3 | probably never — they run cargo/hyperfine |
| forge | 7 | last, and some may legitimately stay |
The 50 is the number that matters and it was not expected: this is a git policy engine, so most gates read git. The "79 gates become Rego rows" framing every capability row inherited is unproven for 60 of the 82. That is this row's first deliverable to settle, not assume.
The pilot
mise-pin-agreement — "every tool version named in .mcp.json agrees with mise.toml's pin — the second place a pin is written cannot drift from the first".
Chosen on data: 107 lines of bash, 10 cases, 108 lines of bats — the smallest of the structured-document gates. Both inputs are formats Fact::Document already parses (CLOUD-772: TOML/YAML/JSON/JSON5), and the predicate is agreement between two parsed trees, which is what Rego is for. No git, no spawn, no stdin.
It also carries no #MUTANT directive, so it is one of CLOUD-480's undeclared gates. Migrating it should add a declared mutation — coverage improves as a side effect rather than degrading.
Rejected as pilot: no-docs-tree, which an earlier plan named. It is not a mise-tasks/ program at all — it is a batten.toml rule in the hk gate. Recorded because the name was carried in prose across three documents before anyone checked the tree.
Sequencing, and the one trap
- Settle the 50. Determine what a git-fact gate needs on the tree surface. Until then "79 gates migrate" is an estimate, not a plan.
- CLOUD-835 lands — the destination for the 1,570 bats cases.
- The pilot, end to end, one gate: Rego module,
test_rules, delete the task and its suite, MUTANT declared. The pilot converts the estimate into a measured cost per gate. Do not batch before it. - Waves by bucket, cheapest first.
The trap, and it is new as of today. CLOUD-807 landed retires_with, so a suite may now be deleted exactly when its declared subject dies. That is correct and it was the precondition for retiring anything — but it makes migrating without CLOUD-835 worse, not better. The ratchet will admit deleting a suite whose task died, with nothing asserting the Rego that replaced it. The permit made coverage evaporation quiet. 835 is a hard blocker, not a nicety.
Refinement — Ready
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Source of truth (§1). The tree. The census below is the authority on progress, not a burndown restated anywhere;
batten.tomlowns which gates are policy rows andmise-tasks/owns which are still bash. No second list of what has migrated. - Computable predicate (§2). The census, re-run at every wave boundary:
ls mise-tasks/ | wc -l ; cat mise-tasks/* | wc -l
grep -l '#MISE description="Gate' mise-tasks/* | wc -l
ls tests/*.bats | wc -l ; cat tests/*.bats | wc -l ; grep -h '^@test' tests/*.bats | wc -l
grep -c 'kind = "policy"' batten.toml
A wave that does not move these down has retired nothing, whatever else it landed. That predicate is the whole point of this row existing: today's +791 was invisible because nothing computed it.
- Effect (§3).
read— migration moves predicates between surfaces; no verb is added and no rule kind'sAuthoritychanges. - Generated artifacts (§4).
schema/batten.schema.jsononly if a row key changes; the per-wavebatten.tomlrows are authored.derived-checkandschema-checkgate both. - Output & exit (§5). Unchanged — a migrated gate keeps its exit contract, and a Rego predicate reports pointer-only by construction. A migrated gate's refusal text must still name its remedy (CLOUD-437); a
msgthat lost the remedy in translation is a regression the bats case would not catch. - Commit / bump (§6).
refactorper wave — no bump. The answers are identical by construction; a wave that changes a verdict is not a migration. - Test obligation (§7). Per wave, and the pilot establishes the shape: (a) every case in the retired suite has a
test_rule that fails when the predicate is wrong; (b) the retired task and its suite are both gone, admitted byretires_withbecause the subject died; (c) the gate's declared mutation is caught bymise run mutant— formise-pin-agreementthat is a mutation it does not have today; (d) the census moves down by the retired count, asserted rather than eyeballed. - Blockers (§8).
blockedByCLOUD-835 — see the trap above; without a test destination the permit lets coverage disappear silently.relatedToCLOUD-833 (the surface, landed), CLOUD-832 (predicate ids, landed), CLOUD-807 (the permit, landed), CLOUD-312 (the 11 hook bodies — the other half of the retirement, and not this row), CLOUD-480 (the undeclared gates a migration should shrink), CLOUD-772 (the document substrate the tree bucket consumes), CLOUD-839 (the capability dispatch that bought the machinery).
Acceptance
- The 50 git-fact gates have a stated verdict: migratable on a named fact, or not, with the reason.
mise-pin-agreementis a policy row; its task and suite are deleted;mutantcatches its declared mutation; the census is down by one gate and ~215 lines.- The measured cost per gate from the pilot is recorded here, and the waves are sized from it rather than from the count.
- Every wave re-runs the census and records the delta.
Found while auditing what the CLOUD-839 fleet landed, by asking the question the capability rows never had to answer: how much bash actually went away.
PRESSURE-TESTED 2026-08-21 — wave 1 cannot dispatch yet, and the reason is a run rather than a reading
CLOUD-835 landed (62719ff, v0.0.100), so the blocker in the trap above is cleared and the 1,570 cases have a destination. Before dispatching a wave on that, the path was walked end to end against the release binary in a throwaway git fixture. It does not hold yet.
What the run showed
Two modules in one enabled bundle. One copied verbatim from policy.rs's own module doc; one written against what rules::tree_document actually builds. A stray.o tracked. Each with a test_ rule in the shape the vendored presets use.
$ batten policy test
policy test: 1 bundle(s), 2 passed, 0 failed EXIT: 0
$ batten check
policy/ msrv-must-be-pinned EXIT: 2
The doc-shaped module passes its test and gates nothing. input.tree.tracked is documented at policy.rs:143-147 and never emitted — tree_document builds documents and missing, and nothing else. Rego makes the failure silent: iterating an undefined path yields no violations, so a dead gate and a clean tree are byte-identical. The test_ rule passes because with input as lets the author fabricate the very shape the engine cannot produce.
That is CLOUD-845, and it is a hard blocker on this row rather than tidy-up: every wave-1 agent starts from that doc, and combined with retires_with the failure mode is green tests, silent gate, deleted bash task that used to work. Ten gates migrated that way would show the census going down while enforcing nothing — the exact number this row exists to make honest.
And wave 1 is smaller than the bucket count suggests
The 20-odd tree gates were re-read by what they open, not by what they invoke. Fact::Document parses TOML, YAML, JSON, JSON5 — and Pkl, declarable-never-parsed.
| reads | gates | migratable |
|---|---|---|
| structured config only | 8 | yes |
| markdown | 4 | no |
.bats / .rs / .pkl text |
5 | no |
| no file literals | 3 | partly — needs the tracked list |
That is CLOUD-846. Wave 1 is 8, not 22. The pilot mise-pin-agreement is in the 8 and is unaffected — both its inputs are parsed formats — so the pilot choice above stands.
BUNDLE W0 — the unblocker. Dispatch-ready now.
Both rows are unblocked, both are rules.rs / facts.rs / policy.rs / schema — one file domain, so one agent, one branch, one draft PR, per CLOUD-839's sizing. Nothing else in the campaign can start until it lands.
Superseded 2026-08-21 by the six-bundle dispatch at the foot of this row. W0's chain grew from two rows to five once the acquisition boundary was traced (CLOUD-849/850/851); it is now bundle A there. The prompt below is still accurate for the 845→846 half and is kept because bundle A's prompt builds on it.
| # | Chain | File domain | PR shape |
|---|---|---|---|
| W0 unblock-migration | CLOUD-845 → CLOUD-846 | rules.rs (tree_document), policy.rs (module doc + policy test), facts.rs, git.rs (list_tree), schema/* |
1 PR |
845 first: it fixes the input the doc promises and closes the false-green class. 846 then adds the lines fact on top of a tree_document that is already correct, and its §5 assertion (a finding may see a line, never carry one) is easier to state once 845's input-shape check exists.
W0 unlocks wave 1 at 8 gates. It does not unlock the other 12: those wait on 846's lines fact landing and being demonstrated, which is 846's own acceptance (d).
Dispatch prompt — one paste, self-contained
You are bundle W0 of the CLOUD-843 bash-retirement campaign in the Batten repo. Read
CLOUD-843 first: it carries the census, the bucket classification and the pilot choice.
Nothing else in the campaign can start until your PR lands.
YOUR CHAIN - one branch, one draft PR, landed in this order:
CLOUD-845 -> CLOUD-846
CLOUD-845 first. There is a REPRODUCTION on that row - run it before you change anything,
and keep it as the test. Two modules in one bundle, one copied from policy.rs's module
doc and one written against what rules::tree_document actually builds: `batten policy
test` reports 2 passed, exit 0, while `batten check` reports only one predicate. The
doc-shaped gate is dead and its test is green.
Three parts, and the third is the one worth having:
- Emit `input.tree.tracked`. Do not just delete the doc example - a tracked-path list is
what a whole class of these gates needs. `git::list_tree` already exists at git.rs:784
and CLOUD-833 already uses it for bundle membership under --config-from. Bound it by
declaration the way `documents` is bounded; an ambient walk would make the `read`
classification a lie by degrees.
- Make policy.rs's module doc true, and assert it: every field its examples reference
exists in what tree_document emits. Same shape spawn_census.rs:216 uses against
clippy.toml. This defect is CLOUD-589's class recurring in the file that landed
CLOUD-831, which was filed for exactly it - so an assertion, not a careful edit.
- `batten policy test` refuses a `with input as` naming a key the engine cannot produce,
at exit 1 (config fault, not a policy verdict). This closes the CLASS. Without it every
field added to the input document reopens the same hole. CLOUD-834 is making the
document's keys the Fact variants asserted by exhaustive match - validate against that
same table, do not build a second list.
Then CLOUD-846 on the same branch: a lines fact, `input.tree.lines[<path>]`, so a module
can decide over a .bats or .md file. Lines rather than raw text, and the reason is rule 4
rather than convenience - a module may SEE a line, a finding may never CARRY one. Assert
that; it is the clause with teeth and the one that keeps pointer-only structural. A
declared path the tree lacks is could-not-look, never an empty array. Acceptance (d) is a
demonstration, not a claim: migrate one of the four markdown gates as proof.
CROSS-BUNDLE: you are the only branch in flight on this campaign. CLOUD-834 is In Progress
in the CLOUD-839 fleet and also touches the policy input document - it projects the Fact
variants into it. Coordinate through that row rather than racing it: if 834 lands first,
rebase and validate against the table it built.
WORKFLOW CONTRACT (AGENTS.md is authoritative; this is the summary):
- Claim by hand BEFORE writing code: `mise run claim-check`, and assign yourself. The
automation fires on the PR event, the end of the work, so waiting for it reserves nothing.
- `git fetch origin main`, short-lived branch, never author on main.
- Commit early and often. You are pre-authorized to commit and push without asking.
- Run the full `mise run verify` after EVERY commit. Local execution is free; a CI run is
metered and the landing lease is fleet-wide.
- Open the PR as a DRAFT immediately (`gh pr create --draft`). CI does not run on drafts.
- When the chain is complete: `mise run linear-check`, then `mise run land` backgrounded.
Do NOT ready by hand - land readies after its push. Do NOT wrap land in bespoke retry or
pre-check logic; main advancing under you is that loop working.
- Background anything that can exceed ~2 minutes; a foreground command is killed at ~2 min.
- Move the Linear row as you move the work. Carry the lifecycle to landed-and-verified
without stopping to report and wait.
Wave 1, after W0 lands
Eight structured-config gates, pilot first: mise-pin-agreement end to end — module, test_ rules, task and suite deleted, a #MUTANT directive declared where it has none today. The pilot converts the estimate into a measured cost per gate, and the waves are sized from that number rather than from the count. Do not batch before it.
A second thing the fixture run turned up, recorded here rather than filed because it is a one-line observation and its home is this campaign's tooling: the protected-path gate matches batten.toml by basename, so it refused writes to a fixture's batten.toml in a temp directory outside the repository — and the advertised BATTEN_GH_GUARD_BYPASS=1 did not take as an inline environment assignment. Every wave-1 agent will hand-build such a fixture. Whoever hits it should file it rather than work around it silently.
DISPATCH 2026-08-21 — six bundles, and the one lever that decides wall-clock
Gates per PR, not agents
The fleet-wide landing lease charges per land, not per gate — one branch spends CI at a time, ci p95 ≈ 701s.
| batching | lease acquisitions for 82 gates | pure landing time |
|---|---|---|
| one gate per PR | 82 | ~20 h |
| one wave per PR | 6 | ~1.5 h |
That 13× is the whole answer to "fastest", and every other choice is noise beside it. Migration is embarrassingly parallel per gate, which makes one-agent-per-gate the tempting and slowest schedule. Fan out the authoring, serialize the landing. Past ~8 concurrent PRs each extra worker adds landing time (every land forces every other branch to rebase) without removing work time, so do not dispatch 30.
Two tracks, run concurrently
The objective is 82 gate tasks and 11 hook bodies (CLOUD-312). They share almost nothing.
Track 2 has a free start: run-shape-guard is 630 lines, opens exactly one file (mise.toml), and is otherwise pure string analysis of command — which the envelope already carries. It needs no Document fact, so it is migratable now, before any capability lands. Earliest census movement available, one land. Its last two families need CLOUD-613, which is Backlog with no Ready block.
The bundles
| # | Bundle | Rows, in order | File domain | Why together |
|---|---|---|---|---|
| A | Acquisition — the long pole, gates all of track 1 | CLOUD-849 → 845 → 850 → 846 → 851 | rules.rs, facts.rs, policy.rs, schema/ |
A strict chain on one function; splitting means agents rebasing onto each other's edits to tree_document |
| B | Ready-block gate family | CLOUD-852 → 842 → 595 → 826 → 751 | mise-tasks/ready-lint, tests/ready-lint.bats |
Five rows, one 35-line §6 block. Any split is a guaranteed conflict for zero parallelism |
| C | Board-gate wiring | CLOUD-825 | the seven board gates + their invokers | Disjoint from B; released is fed /dev/null and three gates have no invoker |
| D | Hook surface | CLOUD-461 → 525 | hook.rs, lib.rs, doctor |
The two capabilities gating contract-drift and stop-guard retirement — the hook half |
| E | Envelope content fact | CLOUD-758 | hook.rs, facts.rs |
Prospective Write/Edit content; the hook bodies reading tool_input need it. Folds into D if the fleet is cut to five |
| F | Instruments + base ref | CLOUD-844, CLOUD-720 | .claude/rules/, resolve.rs |
Two small independents |
CLOUD-852 has landed (PR #625, b405ca8) — bundle B starts at 842.
B is the highest-leverage non-obvious bundle. It is not on the objective's critical path, it is on the throughput path: every row of every later wave passes ready-lint, and that gate misread a negation, cannot check the claim it reports checking, refuses a corpus it was changed out from under, and accepts a §7 naming tests that do not exist. 82 migrations run through it is 82 chances to ship a Ready block nobody can trust.
Order
T+0 — five agents. A is 5 deep and lands last; the rest are shallow and clear the lease before A needs it. Agent 5 takes the run-shape-guard partial migration plus C.
T+1 — after A lands. One agent, one PR: the mise-pin-agreement pilot plus all 8 structured-config gates. Not the pilot alone — its purpose is the measured per-gate cost, measured just as well inside a batch of 9, for one lease instead of two.
T+2 — three parallel PRs: the 12 lines-fact gates, the git-fact gates this row's verdict clears, and the remaining hook bodies.
T+3 — the git remainder. ~10 lease acquisitions total.
What gates the schedule, and neither is on the bundle list
- A must land first and nothing parallelises it. Every hour it slips slips all of track 1.
- CLOUD-480 must land before wave 2, not after. Batching 8–24 gates per PR means one false-green module hides inside a large green diff;
mutantat its current coverage cannot see it, andretires_withadmits the suite deletion anyway. Batching raises the value of the anti-false-green instrument, so it comes first. - Waves 1–3 have no owner. This row's Acceptance stops after the pilot. Either it grows to carry them or a sibling row does — dispatching a wave against a row that does not claim it is how work lands with nothing recording that it did.
Dispatch is BY HAND, and that is settled
create_session is refused upstream: the session-management tools carry a mandatory-approval flag — "requires explicit approval regardless of permission mode" — and bypassPermissions, an explicit permissions.allow entry and a PreToolUse allow hook are all recorded as tested and failing (#76264, #87548). mem:connector-allowlist-recovery's STOP section carries the mechanism and the tell. Do not spend a turn re-attempting it. A human opens the sessions and pastes the prompts; each bundle's rows carry full Ready blocks, so a prompt need only name the chain, the file domain and the workflow contract.
CLOUD-418 A new gate is never shown to fail, so a test that cannot discriminate ships as coverage
Why
This repository's most-repeated failure is a claim nothing exercises. land's refusal branch was dead code for months (CLOUD-235). timeout-check's budgets were placeholders that could not fire (CLOUD-352). A shape rule whose pattern was a program could never match and read as coverage (CLOUD-401). Each was caught after the fact.
It happened again, live, while building the landing lease (CLOUD-393). A concurrency test was written for a real race — observe() reading FETCH_HEAD, which is one file per clone while the heartbeat runs beside held/release in the same checkout. The test was green. Then the buggy version was restored to check the test could catch it, and it passed on the broken code too: every process fetches the same lease ref, so a crossed read yields a different generation of the same lease rather than an observably foreign one. The test asserted nothing.
That was found only because someone chose to mutate and re-run — a discipline nothing asks for and nothing checks. The green suite before that check and the green suite after it were indistinguishable.
Root cause. The obligation is stated as "a rule ships with a runnable gate" — a gate that exists. Nothing requires evidence the gate discriminates. A test that passes on both the fixed and the broken code satisfies every rule this repo currently has.
Scope, deliberately narrow. Not mutation testing over the workspace, which is a research project and a large CI bill. The claim here is about mise-tasks/*-check and the guards — the files whose entire purpose is to refuse — where the mutation is usually a one-line inversion and the suite is bats, so a run is seconds.
Refinement — Ready
- Source of truth (§1). The gate's own suite, run against a deliberately broken copy of the gate. A pass there is the defect; the verdict is an exit code, not a judgement.
- Mechanism (§3). Undecided between two, and choosing is what Ready needs:
- Author-side, checked in. Each gate declares one or more
mutantcases — a stated one-line corruption and the test name that must go red. A task runs them, and a mutant nothing catches fails. Costs the repo one small fixture per gate; runs locally, off the landing path. - Scheduled sweep. A weekly job applies mechanical mutations to
mise-tasks/*-checkand reports any whose suite stays green. No per-gate authoring, weaker coverage, and it belongs besidebranch-age-checkin the hygiene sweep, so no new CI minutes.
- Author-side, checked in. Each gate declares one or more
- Deliberately not in scope (§2). Mutation coverage of
crates/. Different tooling, different cost, different question. - Output (§7). Pointer-only: the gate, the mutant, and the test that failed to notice. Never a diff of the mutated source.
Test obligation
The mechanism must catch the case that motivated it: the FETCH_HEAD mutation of mise-tasks/land-lock against tests/land-lock.bats as it stood before the structural assertion replaced it. That pair is a known-good fixture — a real gate, a real mutant, and a real suite that missed it.
Commit / bump (§6): feat(gate) — patch until 0.1.0 regardless of type.
Blockers (§8): none.
Acceptance
- Every
*-checktask has at least one mutation its suite is proven to catch. - A gate whose suite passes on a broken copy fails.
- The
land-lock/FETCH_HEADpair is covered as a regression fixture, so the case that motivated this cannot recur silently.
CLOUD-683 memories-check forces every memory into AGENTS.md, whose line budget is full, so the repo can no longer add a memory at all
Measured 2026-08-19. Two individually-green gates compose into a deadlock:
memories-check'smemory-unindexedconjunct (CLOUD-291) requires every tracked memory to appear as a row inAGENTS.md.batten.toml[budget.instructions]capsAGENTS.mdatmax_lines = 199.AGENTS.mdis at 199. Adding one row makes it 200 andpolicy-budgetexits 1.
This repo cannot add a memory. Witnessed: CLOUD-682's finding could not be written to its own memory and was folded into workflow/agent-fanout — the wrong home — purely because that memory already had a row. Commit ff27464 carries that compromise.
Root cause: a misread premise
CLOUD-291 §1 justifies the flat table:
"the index ... stays the one place a trigger is written — this change adds no second list of memories and no per-memory metadata, because a trigger declared inside a memory is exactly what the upstream convention forbids."
memory_maintenance.md, the shipped Serena convention, says:
"Memories themselves should not contain information about when to read them; this is the responsibility of the referring memory."
The convention forbids a memory declaring its own trigger. It explicitly expects a memory to carry another memory's trigger — a graph rooted at mem:core. CLOUD-291 read that as "only the always-loaded index may carry triggers" and collapsed the graph into one flat table inside the one file under a hard line budget. That is what couples memory count to AGENTS.md length.
The gate's own comment anticipates the fix and dismisses it:
"a referencedness check written against
mem:mentions could not work either — the graph root carries zero outgoing references and would fail the tree it governs."
That objection does not hold. The root is exempted, exactly as memory_maintenance.md is already exempted from both existing conjuncts.
Measured starting state
core.md carries zero outgoing mem: references; 1 of 11 memories is reachable from it. The graph the convention describes has never been built here.
Refinement — Ready
- Source of truth (§1).
mise-tasks/memories-check, which already owns every property asserted over the memories tree. No second list and no per-memory metadata are added — the trigger moves from a table row to a referring memory's prose, which is where the convention puts it. - Mechanism (§2). A union, not a replacement: a memory is indexed iff it is reachable from
coreviamem:references, or it appears as a row in theAGENTS.mdtable. Strictly more permissive than today, so nothing turns red and the table drains incrementally. BFS seeds withcoreplus every memory referenced from tracked markdown outside.serena/memories/. Root and template exempt, reusing the existing$templatevariable so the exemptions cannot drift. - Effect (§3).
read— inspects tracked files, mutates nothing. Unchanged. - Lifecycle (§3).
memories-checkcomes offpre-commitand runs atcheckonly, therefore inverifyandci. A memory written in an ephemeral container is harmless; only landing one is destructive, and the gate is currently at the wrong point in the lifecycle. Needs a secondMappinginhk.pkl, sincepre-commit,checkandfixshare onegatetoday. - Output & exit (§5). Pointer-only,
file:0 rule-id (name), exit0/1. Unchanged. - Test obligation (§7).
tests/memories-check.batsgains: a memory reachable only through a chain fromcorepasses; a genuine orphan is still reported with a pointer; the root is not reported against itself; the template stays exempt with no row and no reference; a fully-connected tree exits 0 — the anti-vacuity case CLOUD-291 rightly insisted on, since this predicate also goes quiet exactly when satisfied. - Blockers (§8). None.
Acceptance
- A memory referenced only from another memory passes the gate.
- A genuinely orphaned memory is still reported.
- Adding a memory requires no
AGENTS.mdedit and no budget change. - A local commit touching only memories is not blocked at
pre-commit. AGENTS.mdregains headroom once its table is drained in favour ofmem:core.
Not in scope
Raising max_lines — the migration removes the pressure, and raising the cap defers it while weakening a budget that exists for a reason. Deleting the table-parsing arm — a follow-up once the table is empty.
CLOUD-1253 builds the gate for the closing half: a closed retirement row judged against the tree, from the `conserves` arms that already exist. It cannot carry the READING half, and the reading half is where the loss happens. A gate runs at `verify` and at close; an agent consults a row's state continuously and acts on it at once, so between two runs of any gate a wrong state is load-bearing prose that an agent trusts. MEASURED TWICE IN ONE SESSION, both mine. `CLOUD-1162` sat In Review with `board-diff-overlap.sh` still tracked, because the merge moves a row the moment a PR attaches to it. My first correction was a warning paragraph inside the body with the state left alone — which is that row's own recorded finding one level up, where a correction block does not correct a title. A state is read by more automation than a title is. `CLOUD-1160` sat In Progress with nothing shipped and no PR. Its only attachment was #804, whose title reads "CLOUD-312 row 10 + CLOUD-1294: session-start.sh retires" — different work, already merged. I read "attachment present + In Progress" as another session's live work and declined to race it, reporting the largest single-program retirement available as taken. The contradicting evidence was in the payload I had already printed. THE THREE CLAUSES, and each names a failure that actually happened rather than one that is easy to imagine: the tree settles it; the row moves BACK to Backlog rather than Todo, since parking an unpullable row in the ready queue hands the next agent work that cannot be started; and the move owes a comment, never a note inside the body. NOT GATED, AND SAID SO. Non-negotiable rule 3 puts "did the agent consult the tree before believing a row" outside what a gate decides — it is a model verdict. So this is feedforward with CLOUD-1253's predicate as the gated half, which is `.claude/rules/scanning.md`'s own shape for its suitability axis. LINE-NEUTRAL, because `[budget.instructions]` was at 199/199. The section it joins is compressed to pay for it: 198 lines and 3405 tokens, three fewer than before the rule existed. No budget was raised to make room. Five cases in the new tier, and the fourth is not decoration on the third: annotating in place is the failure that happened, so a reader who takes "move it back" as satisfied by an explanatory paragraph has made the same mistake. The fifth is the anti-vacuity one — it pins the text inside the board section of the always-loaded file, since the other four would pass just as well over a rules file that loads at a trigger, and "I am about to trust a row's state" has none. Shown able to fail on the real file rather than a fixture: replacing one clause turned exactly its own case red with the other four green, and restoring it returned all five. Refs: CLOUD-1305 Refs: CLOUD-1253
8d707f5 to
1cae2a5
Compare
|
❌ The last analysis has failed. |
|
Warning Review limit reachedNext included review available in 49 minutes. View limit detailsLimit details: You’ve used the included review currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (3)
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
|
/fast-forward |
Why
CLOUD-1253builds the gate for the closing half: a closed retirement row judged against thetree, from the
conservesarms that already exist. It cannot carry the reading half, andthe reading half is where the loss happens. A gate runs at
verifyand at close; an agentconsults a row's state continuously and acts on it at once. Between two runs of any gate, a
wrong state is load-bearing prose that an agent trusts.
Measured twice in one session, both mine
CLOUD-1162sat In Review withboard-diff-overlap.shstill tracked — the merge moves arow the moment a PR attaches to it. My first correction was a warning paragraph inside the
body with the state left alone, which is that row's own recorded finding one level up:
"A correction block does not correct a title, and the title is what a queue listing and every
inline reference render." A state is read by more automation than a title is.
CLOUD-1160sat In Progress with nothing shipped and no PR. Its only attachment was #804,titled "CLOUD-312 row 10 + CLOUD-1294: session-start.sh retires" — different work, already
merged. I read "attachment present + In Progress" as another session's live work and declined
to race it, reporting the largest single-program retirement available as taken. The
contradicting evidence was in the payload I had already printed.
That second one is a sub-shape
CLOUD-1253's table does not cover: not a wrongly-closed rowbut a stale In Progress, which has no drain (
CLOUD-469) becauseboard-sweephas noinvoker (
CLOUD-825).The rule
Each clause names a failure that actually happened rather than one that is easy to imagine.
Backlog, not Todo: Todo is the ready queue, so parking an unpullable row there hands the
next agent work that cannot be started.
Not gated, and it says so
Non-negotiable rule 3 puts "did the agent consult the tree before believing a row" outside
what a gate decides — it is a model verdict. So this is feedforward with
CLOUD-1253'spredicate as the gated half, which is
.claude/rules/scanning.md's own shape for itssuitability axis. No gate is added and
done-checkis untouched.Line-neutral against a hard ceiling
[budget.instructions]was at 199/199 lines. The section the rule joins is compressed topay for it: 198 lines, 3405 tokens — three fewer than before the rule existed. No budget
was raised to make room, which is
CLOUD-683's constraint.Shown able to fail, on the real file
Five cases in
crates/batten/tests/it/board_state_claim.rs. Replacing one clause turnedexactly its own case red with the other four green; restoring it returned all five. Both
directions observed against
AGENTS.mditself rather than a fixture.Two of the five are worth naming:
refuses_the_annotate_in_place_shortcutis not decoration on the move clause. Annotatingis the failure that happened, so a reader who takes "move it back" as satisfied by an
explanatory paragraph has made the same mistake.
the_rule_lives_where_it_binds_every_turnis the anti-vacuity case. The other four readAGENTS.mdby path and would pass just as well if the section were relocated into a.claude/rules/*file — which loads at a trigger, and "I am about to trust a row's state"has none. It pins the text inside the board section of the always-loaded file.
Board actions already taken, not deferred to this PR
CLOUD-1162→ Backlog, unassigned, with a comment recording thegit cat-filecheck.CLOUD-1281→ Duplicate ofCLOUD-841. A previous PR body claimed it was already markedso; it was not, until now.
CLOUD-1253→ a comment carrying both instances and the sub-shape its table lacks.Closes CLOUD-1305
DO-NOT-CLOSE CLOUD-1253
Refs: CLOUD-418
Refs: CLOUD-683
Generated by Claude Code