feat(facts): a vendored prompt's dispatch becomes a fact the tree can read - #826
Conversation
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
**Measured on this repo's own work, 2026-08-12, on **CLOUD-369. Its Ready block carried:
The first half shipped and is tested. The second half was never implemented and never tested. The landed The failure is structural, not inattention. Reconstructing 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 treeThis 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 oneA 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 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. So the defect is not a missing mechanism. The escape is that the prose dialect stands as an ALTERNATIVE rather than as a legacy.
2. Where it runs — committed, and the declared set reaches the tree through the recorderDecided by a property measured while landing CLOUD-1302: the
3. Ready AND land — two predicates, not one placementThe body treated this as either/or. It is neither.
4. Deferred-but-covered — the
|
| gate | s | what its verdict is about |
|---|---|---|
renovate-config-validator |
32.3 | renovate-config-validator's findings over renovate.json |
pkl-check |
5.4 | pkl over hk.pkl |
hook-profile-check |
0.6 | the hook profile validator |
| 38.3 |
CLOUD-1199's filled second-input table records the same gap from the wrapper family's side;
CLOUD-1163 unit 6 (token-bench-drift) is a fourth consumer, an identity check the same
channel is the answer for.
What to build, and the one property that must not be got wrong
A producer verb — batten tools record — mirroring the shape receipt record and
state record already use. It runs the validator outside the engine (a mise task, a CI
step), because §5 makes check read and structurally incapable of spawning, and writes the
keyed record this reads back.
It must digest the input ITSELF, never accept a caller-supplied digest. That is the whole
anti-staleness property: tools.rs::verdicts reads the subject from root and digests it
there precisely "because the digest is what makes the record stale-by-construction and a*
*caller that supplied one could supply the wrong one." A producer taking a digest on the
command line hands that guarantee back to the caller and the family stops working.
There is one parser for the record's line shape — crate::forge::parse — and a second
would be two authorities over the same bytes.
Refinement — Ready (the write half of a landed read; no new fact, no new module)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
-
**Authority boundary (§1). **
crates/batten/src/tools.rsgains the write path; the verb is
registered incrates/batten/src/surface.rs.policy/validator-verdict-clean.regois
unchanged — it is already correct and is the consumer, not the defect. No
mise-tasks/program and notests/**/*.batsis added or edited. -
Computable predicate (§2). After a producer run over a declared
[[rule.tool]]row, a
freshbatten checkon the same tree resolvesinput.tree["tool-verdict"]to an object
carrying that row's id — and to a different answer once the input's bytes change. -
The discriminating pair (§2). A validator that found something must produce a REFUSAL
through the module, and a clean one must not. Today both are silent, which is the whole
bug: an assertion that only proves the module loads passes over the dead version. -
Deliberately not in scope (§2). Changing what any validator decides. Making absence a
refusal —validator-verdict-clean.rego's METADATA already records that as a deliberate
direction and a separate predicate. Retiring the three gates above; each is its own row and
each consumes this. -
**Effect (§3). **
write— it spawns the validator and writes under$GIT_DIR. It is on
the spawning side of §5's split, whereperfalready sits, and it is emphatically not
check. -
Output and exit (§5). Pointer-only: the finding's NAME and a
path:line, a count or a
status token. Never the validator's report or its diagnostic prose —tools.rs's own
header says a validator's output is the likeliest place in this family for a secret to
appear, so non-negotiable rule 4 is decided at the boundary rather than at the report. -
**Commit / bump (§6). **
feat(facts)— patch until0.1.0. -
Test obligation (§7). Over the compiled binary, and the second tier is the one that**
**matters here — awith input ascase is what let the dead version ship. Three
discriminating observations, perCLOUD-418: the producer writes a record and a subsequent
checkREFUSES over a finding; the same over a clean verdict does NOT refuse (the
anti-vacuity mirror); and a record written before the input changed is not found rather
than found-and-wrong, which is the keying the whole family turns on. -
Blockers (§8). None. It
blocksthe three retirements above.relatedToCLOUD-1171
(which landed the read half),CLOUD-845(the dead-gate class),CLOUD-1253(Done while
the condition persists — this is an instance its own §2 predicate cannot reach, since the
row carries noconservesarm). -
Weakens (§8), groomed late and recorded as such. The producer's caller is a new inline
mise.tomltask body, whichinline-task-bodies-not-growingcounts (31 -> 32) and denies. The two dispositions its ownno_fix_reasonnames are "migrate the predicate onto a rule kind, or waive the increase deliberately with a reason and an expiry" — and the predicate did migrate: whatpkl-checkdecided ispolicy/validator-verdict-clean.rego's now, off theconfig-validatorrow. What is left inline decides nothing; it RUNS the validator, which §5 makes structurally impossible inside the engine, so there is no rule kind for that body to become.mise-tasks/pkl-check.shand its 76-line suite leave the tree in the same commit, so the bash surface falls while amise.toml-only count rises by one — the shape the row's own header warns about, where "a row that denies its own campaign's wins is worse than the hole it closes". Unnarrowed, because a ratchet finding's path is the glob plus the counts; expiring 2026-10-31, and expected to lapse unused, sincebase = "origin/main"makes the floor 32 the moment this lands.Recorded honestly: this clause was groomed after the work started, on 2026-08-31, once
config-lintrefused the change — the same sequenceCLOUD-1164records for its ownWeakensclause. The alternative (collapsing the task body to a one-line shim) was examined and declined: it drops the/root/.ccr/ca-bundle.crtfallback carried verbatim out of the retired program, which is the arm this container actually takes, so a validhk.pklwould fail as though malformed.**Weakens: **
waiver-addedatwaiver[inline-task-bodies-not-growing] -
Weakens (§8), the second clause, and it is a STRENGTHENING the gate cannot tell from a weakening. The producer needs a second declared subject to be proven on at all, so
validator-verdict-cleangains a sibling[[rule.tools]]row (renovate-config,renovate-config-validatoroverrenovate.json5) beside the landedconfig-validatorrow.config-lintreads any edit to an existing rule's predicate asrule-predicate-changedand refuses it againstorigin/main, which is correct by design: house style §8 loads policy out of band precisely so a branch cannot lower the bar it is judged by, and a gate that tried to distinguish "added a subject" from "removed one" would be judging intent rather than bytes. So the admission is groomed rather than argued at PR time.The alternative — a rule of its own, which
hook-profilein this same change takes and which produces no smell — was examined and declined for the sibling: what counts as a clean verdict is the same question for both validators, and two rules over one module is two adjudications of one question.hook-profileis separate for the opposite reason, that it reads present-and-empty as a REFUSAL where a validator reads it as clean.Recorded honestly: groomed after the work started, on 2026-09-01, once
config-lintarmed againstorigin/mainrefused it underverify. An earlier unarmed run had reported0 smell(s), which is what let it reachverifyat all — the unarmed form does not compute the base-ref class, and reading its silence as a pass was my error rather than the gate's.**Weakens: **
rule-predicate-changedatrule[validator-verdict-clean].tools
Acceptance
batten tools recordexists, digests its input itself, and writes through the one parser.- A deliberately-broken subject produces a refusal end to end over the compiled binary; the
same subject repaired produces none. - A record whose input has since changed is absent rather than answering.
- At least one of the three gates above is shown unblocked as a consequence.
- No validator output text appears in any finding, asserted.
Found while filling CLOUD-1199's second-input column: the tool-verdict class read as
unblocked because the fact had landed, and the fact had landed with nothing to fill it.
CLOUD-1314 The two CLOUD-605 identity hooks are still wired, and the issue that recorded their precedence closed without removing them
Context
policy/harness-wiring.rego carries a declared table whose contract is "what is wired today that should not be, naming the issue that owns its retirement". Two of its three rows name CLOUD-605, which is Done (2026-08-23):
~/.claude/stop-hook-git-check.sh~/.claude/session-start-git-identity.sh
A licence keyed to a closed issue is the permanent-exemption shape the table exists to refuse. Measured on this branch while landing CLOUD-1310.
What CLOUD-605 actually closed
It closed by recording a precedence — [attribution] identity_deny outranks a harness identity request, now non-negotiable rule 8 in AGENTS.md, gated by no-denied-identity-prescribed. That mechanism stops the hook's remedy from being followed. It does not remove the hooks, and it could not have: both live in ~/.claude/, outside this repository, so turning them off is an owner action on the environment config.
So the row was right to exist and wrong to point at CLOUD-605: the issue that owns the precedence is not the issue that owns the removal, and nothing owned the removal.
Ready
Mechanism as a computable predicate. The two rows in harness-wiring.rego's declared table point at this issue instead of CLOUD-605, and harness-wiring regains the direction that refuses a row whose owner has closed — the direction hooks-wiring-check used to hold from piped-in payloads and which CLOUD-1160 retired with no successor. CLOUD-1310's Fact::Minted is what gives that predicate a tree-surface home.
Done when: both hooks are gone from the harness config, the rows are deleted from declared, and harness-wiring refuses their return.
Out of scope
Re-litigating rule 8. The precedence stands; this is only about the two programs still being wired underneath it.
CLOUD-1079 Stop provisioning the two user-level git hooks into `~/.claude/launcher-settings.json` — the owner action CLOUD-605 named and nothing owns
Why
~/.claude/launcher-settings.json registers two programs this repository does not own and cannot unregister:
SessionStart: ~/.claude/session-start-git-identity.sh
Stop: ~/.claude/stop-hook-git-check.sh
CLOUD-605 established that a delete does not survive, and named the remedy without owning it: "the only place it can actually be turned off is the environment configuration that generates launcher-settings.json — outside this repository, an owner action. That is the real remedy." This row is that action. It exists so the remedy has a home rather than being a sentence inside a Done issue.
Re-provisioning, measured 2026-08-27. A previous session ran batten wiring reclaim -y, which removed both registrations from the merged surface. This session's files carry identical mtimes of 22:36:38 — launcher-settings.json and both scripts, rewritten together at session start. So the repair and its erasure are one event, and no in-session action can outlast it.
Why each should stop being provisioned, for this environment
stop-hook-git-check.sh is unsatisfiable here by construction. Its predicate (line 83) is an OR:
if [[ "$ce" != "noreply@anthropic.com" ]] ||
! git cat-file commit "$sha" | grep -qE '^gpgsig(-sha256)? '
Commits here are SSH-signed, so the second term is already satisfied and the email term alone carries the refusal. The only value it accepts is the one [attribution] identity_deny forbids, so the hook exits 2 on every correctly-attributed commit and the remedy it prints produces a commit commit-attribution refuses. Two policies, one of which has to not run.
session-start-git-identity.sh sets user.email, user.name and core.hooksPath globally (lines 21, 22, 104). The identity half is redundant here — attribution-identity writes repo-locally and local beats global, which is CLOUD-605's own recorded reason attribution in this repo has never failed.
The core.hooksPath **line is new information and changes the shape of the ask. **CLOUD-605 describes this script as an identity setter; it is also a global git-hooks redirect. So removing it wholesale has a blast radius beyond commit identity, and this row asks the owner to decide per line rather than per file. Turning off the identity pinning is what this repository needs; the hooks-path line may be load-bearing for the container and is not ours to judge.
What this blocks
The hook-registration campaign's remaining half. With these two present, hooks-wiring-check under a [hook] exclusive declaration can only be satisfied by deleting another tool's user-level configuration — which is the delete-and-lose race above. Once they stop being provisioned, the campaign's strong reading and the gate agree with no exemption and no delete, which is the only state where zero siblings is true rather than asserted.
Refinement — Ready
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Source of truth (§1). The environment configuration that generates
~/.claude/launcher-settings.json— a console/provisioning surface outside every repository, which is why no gate in this tree can see or assert it.batten doctor hooks -J'smerged_siblingsis the observation this repository can make. - **Computable predicate (§2). **
batten doctor hooks -Jreportsmerged_siblings: 0forclaude-codeon a fresh container, withmerged_surfaces_readnon-zero so the census is known to have looked rather than failed to. Both numbers, because a zero from a surface nobody read is the false green this whole census exists to refuse. - Effect (§3). No change in this repository beyond the follow-up in §7. The action is a configuration change in the environment that provisions containers.
- Generated artifacts (§4). None.
- Output & exit (§5). Unchanged.
- **Commit / bump (§6). **
fix→ patch until0.1.0, for the follow-up that removes the two now-obsoleteDECLAREDrows. Notchore: release-plz giveschoreno bump at all, and this is not housekeeping — deleting an exemption changes the gate's verdict on an input it previously excused. - Test obligation (§7). After the environment change: a fresh container reports
merged_siblings: 0, and the twostop-hook-git-check.sh/session-start-git-identity.shrows come out ofhooks-wiring-check.sh'sDECLAREDtable in one commit, with the gate green because the siblings are genuinely gone. Shown able to fail by re-adding a merged registration to a fixture$HOME, whichtests/hooks-wiring-check.batsalready drives throughHOOKS_WIRING_MERGED. - Blockers (§8). None in this repository. The action itself needs an owner with access to the provisioning configuration.
Done
A fresh container carries no non-batten hook registration on any merged surface, batten doctor hooks reports merged_siblings: 0 having read the surfaces, and the two DECLARED rows are deleted rather than excused.
What is deliberately not proposed
Deleting the files from $HOME at session start, from this repository or from batten wiring reclaim. That is the race CLOUD-605 measured and it loses: the launcher rewrites them at the next session, and one of the two also sets core.hooksPath for the whole container. A repository fighting its own harness's provisioning is a worse failure than the registrations it is trying to remove.
CLOUD-845 A module copied from `policy.rs`'s own doc passes `policy test` green and gates nothing: `input.tree.tracked` is documented, never built, and a `with input as` test fabricates the shape the engine cannot produce
Why
Reproduced end to end against main @ 6741eab, with the release binary, in a throwaway git fixture. This is not a reading of the code — it is a run.
The reproduction
crates/batten/src/policy.rs:143-147 — the module doc for the violation shape, which is the example an author writing their first module will copy:
violation contains {"rule": "no-stray-artifact", "msg": "a tracked build product"} if {
some p in input.tree.tracked
endswith(p, ".o")
}
Two modules in one enabled bundle. One copied verbatim from that doc; one written against what rules::tree_document actually builds. A stray.o tracked in the fixture. Each module carries a test_ rule in the shape the vendored presets use — with input as {...}, the author supplying the input.
$ batten policy test
policy test: 1 bundle(s), 2 passed, 0 failed
EXIT: 0
$ batten check
policy/ msrv-must-be-pinned
EXIT: 2
Both tests pass. Only one predicate decides anything. no-stray-artifact reports nothing, with stray.o tracked, on a tree-scoped bundle whose row is severity = "deny".
Why, and both halves are needed to produce it
1. input.tree.tracked does not exist. rules::tree_document (rules.rs:2913) builds exactly two keys:
serde_json::json!({ "tree": { "documents": …, "missing": … } })grep '"tracked"' across rules.rs and policy.rs returns 0. The doc comment names a field the implementation never emits. This is CLOUD-589's class — a doc comment citing something that does not exist — recurring in the same file that days earlier landed CLOUD-831, which was filed for exactly that defect in policy.rs's other module-doc paragraph.
2. Rego makes it silent. Iterating an undefined path yields nothing, so the rule body is undefined, so the violation set is empty. A dead gate and a clean tree are byte-identical on the decision surface. This is CLOUD-251's vacuous pass, arriving through the documentation.
3. policy test cannot catch it, because the test supplies its own input. with input as {…} is OPA and Conftest's own shape and is right for a preset, which ships with no consumer tree. But nothing checks that the fabricated input is a shape the engine can produce. The test asserts the predicate fires against {"tree": {"tracked": [...]}}; the engine never builds tracked; the test is green and the gate is dead.
The safety net does fire in the narrow case, and that is worth recording because it shows the gap precisely. With no test_ rules at all, policy test reports:
tree-policy predicate-unexercised no-stray-artifact
tree-policy module-untested policy/from-the-doc.rego
So CLOUD-835 and CLOUD-647's sweep catch no test. They do not catch a test against an impossible input — and the second is what an author following the doc will write, because they will copy the input shape from the same paragraph they copied the predicate from.
Why this blocks the retirement rather than being tidy-up
CLOUD-843's wave 1 is ~20 tree-scoped gates. Every agent doing one starts from policy.rs's module doc. The failure mode is green tests, silent gate, and a deleted bash task that used to work — because CLOUD-807's retires_with now admits deleting a suite whose subject died. The permit made this failure cheaper to reach, not harder. A wave that migrated ten gates this way would report ten passing suites and enforce nothing, and the census in CLOUD-843 would show the bash going down.
The fix, in three parts
- Build the field, do not just delete the doc. A tracked-path list is what a whole class of these gates needs —
no-docs-tree-shaped predicates over which files exist, and the three wave-1 gates that read no file literals at all.git::list_treealready exists (git.rs:784) and CLOUD-833 already uses it for bundle membership under--config-from. Emitinput.tree.trackedfrom it, bounded the waydocumentsis (declared, never an ambient walk) or thereadclassification degrades by degrees. - The doc becomes true either way. If the field is not built, the example must not use it.
- The class, which is the part worth having:
policy testrefuses awith input aswhose keys the engine cannot produce. The input document's shape is already derived rather than designed — CLOUD-834 makes its keys theFactvariants, asserted by exhaustive match. The same table is what a test's fabricated input should be validated against. Without this, every future field added to the document reopens the same hole.
Refinement — Ready
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Source of truth (§1).
rules::tree_documentis the one place the tree input is built;policy.rs's module doc is a reader of it and must not describe a field it does not emit. The validation table for a test's input is the same onetree_documentbuilds from — named once, not restated in the test harness. - Computable predicate (§2). Three, each decidable: (a)
input.tree.trackedis emitted, fromgit::list_tree, bounded by declaration; (b) every field the module doc's examples reference exists in whattree_documentemits — an assertion over the doc text against the emitted key set, the shapespawn_census.rs:216already uses againstclippy.toml; (c)batten policy testrefuses awith input asnaming a key outside the emitted set, at exit1(a config fault, not a policy verdict). - Effect (§3).
read.list_treeis the same bounded, fixed-argv git querybundle_membersalready makes. No new verb, noAuthoritychange. - Generated artifacts (§4).
schema/batten.schema.jsonif a row key changes; drift-gated byschema-check. - Output & exit (§5). Pointer-only: the offending key and the module path, never the module body. Exit
1for the refusal in (c) — it is a config fault at load, not a violation, per the one table. - Commit / bump (§6).
fix(policy)— patch until0.1.0. - Test obligation (§7). The reproduction above is the test, and it is already shown able to fail (CLOUD-418): the doc-shaped module with its doc-shaped test currently reports
2 passedand denies nothing. (a) that same pair goes red after the fix — either the predicate fires or the test is refused; (b) a module usinginput.tree.trackedagainst a fixture with a matching tracked file denies; (c) awith input asnaming a fabricated key is refused, and the refusal names the key; (d) the doc-example assertion goes red when an example references an unemitted field. (c) and (d) are the ones that close the class rather than the instance. - Blockers (§8). None.
relatedToCLOUD-833 (which builttree_document), CLOUD-835 and CLOUD-647 (whose sweep catches no test but not an impossible test), CLOUD-589 (the doc-cites-what-does-not-exist class, now recurring in the file that fixed its last instance), CLOUD-418 and CLOUD-251 (the false-green and vacuous-pass shapes this is an instance of), CLOUD-843 (the campaign this blocks).
Acceptance
- The doc-shaped module either denies over a tracked
.o, or the doc no longer shows a field the engine cannot emit — and a test asserts whichever is chosen. - A
test_rule whosewith input asnames a key the engine cannot produce is refused, naming the key. - An example in
policy.rs's module doc referencing an unemitted field turns a test red. - The reproduction in this row, re-run, no longer reports
2 passed, 0 failedbeside a silentcheck.
Correction 2026-08-21 — git::list_tree is the wrong source for this fact, and the row above names it.
The fix section says "emit input.tree.tracked from git::list_tree". Verified against the tree, that is wrong in two ways:
- It is gix in-process, not a subprocess —
git.rs:784usesopen/rev_parse_single/peel_to_tree/lookup_entry_by_pathundergix::open::Options::isolated. The §3 effect clause reasoning from "the same bounded, fixed-argv git querybundle_membersalready makes" describes a spawn that does not happen. The classification (read) is unchanged; the justification was inaccurate. - It is non-recursive — one directory level only (
git.rs:770-772), because its single production caller ispolicy.rs:982's bundle membership under--config-from, which wants the files in one folder. Atrackedfact needs the whole tree.
So the source is two sources. For the working tree, rules::tree_files (rules.rs:4211-4256) is the existing ignore walk, already hoisted once per run at rules.rs:2706. For --config-from <ref>, a recursive ref walk does not exist — this row either adds one or states that tracked is could-not-look under --config-from. The second is a legitimate answer and must be stated rather than reached by accident, since silently empty is the exact failure this row exists to kill.
A second false-green channel, on the same surface.
policy_rule (rules.rs:2975-2982) returns NotObserved::RuleSkipped if missing carries any entry, and three unrelated failures funnel into missing undistinguished:
- an extension
facts::Formatdoes not know — checked atrules.rs:2917-2923before any I/O, soCLAUDE.md,hk.pkland any.batssuite skip the rule without the file ever being opened; - a read error — ENOENT, EACCES, EISDIR and non-UTF-8 all collapsed by one
let Ok(...) elseatrules.rs:2924, which its two sibling acquisition sites (:3962,:4110) do distinguish; - a parse failure —
Look::IsNotmerged withLook::CouldNotLookatrules.rs:2928-2937.
So a migrated gate can go silent-and-green by declaring a path with the wrong extension, exactly as it can by naming a field the engine never emits. Same failure, second road, same retires_with-admits-the-deletion consequence. Distinguishing the three is in scope for this row: it is the tree document's own could-not-look contract, and §5's exit-1 refusal has nothing to attach to while all three look identical.
§8 is amended: blockedBy CLOUD-849, which collapses the three Fact::Document acquisition sites into one. Building tracked and splitting missing's three causes both edit the acquisition boundary, and doing it here would mean doing it in the one site that is about to be merged with two others.
CLOUD-1337 A backgrounded wait that polls a local process is admitted by `waits_on_condition`, so 11 duplicate watchers ran for 9 hours and AGENTS.md's "refused by run-shape-guard" was false
Why
Measured on this container, 2026-09-02, by a human noticing the process list rather than by any mechanism.
Eleven background waits were running, the oldest for 9h 35m. Every one had this shape:
until ! pgrep -f "mise run verify" >/dev/null; do sleep 20; done
pgrep -f matches against the FULL command line, and the waiter's own command line contains the literal string mise run verify. Each process matched itself. The condition was false by construction, so the loop could never exit — a dead predicate wearing a live shape.
Only one real job was running the whole time. The other eleven were pure spin.
Why nothing refused it — and the FIRST FRAMING OF THIS ROW WAS THE WRONG LEVEL
Filed first as "a self-matching pgrep -f", which is true and is not the defect. Corrected here rather than edited away, because the wrong rule was one commit from landing.
The narrow reading. pgrep -f matches FULL command lines. A mediated call runs as bash -c '<the whole text>', so the pattern is a substring of the polling shell's own command line BY CONSTRUCTION — it is the argument. The probe matched itself, the negation was false on every iteration, and the loop was unbounded.
Why refusing THAT would have made things worse. The remedy for a self-match is to bracket the pattern ([m]ise run verify). A gate that refused only the self-match would have been satisfied by that one character — and the result is eleven correctly-functioning watchers instead of eleven broken ones, every one still redundant. It fixes the typo and blesses the waste.
The actual defect is that waits_on_condition exempts ANY condition. It exists so a sleep loop escapes background-timer, on the sound reasoning that a loop testing a condition exits on the condition rather than on the clock. That is right for a condition nothing else reports — a CI run, a remote queue, a file another machine writes. It is wrong for a LOCAL PROCESS, because the harness already re-invokes the caller when a backgrounded task exits. Polling one duplicates a notification that is already guaranteed to fire.
The exemption asks whether there is a condition, never what the condition is about. until was the escape.
And AGENTS.md's claim about this was FALSE. It states the rule and asserts a mechanism:
The exit notification IS the wake-up; waiting for it costs nothing. … Manufacturing your own wake-ups with a backgrounded
sleep N; tail logis a timer where an exit condition belongs, duplicating the notification (490 in one session, 2 changed a decision).
The last clause is the one to read: "refused by run-shape-guard". It was not. The measurement was recorded, the rule was written, the mechanism was named — and the named mechanism let the shape through, because waits_on_condition had already exempted it. An agent read that paragraph, agreed with it in chat, and wrote the wait eleven times in the same session. That is non-negotiable rule 2's own case against itself: prose is feedforward only; a log without a gate is sensor only — with the additional sting that the prose CLAIMED a gate.
What to build
Narrow waits_on_condition, or add the arm beside it: a backgrounded sleep loop whose condition reads the LOCAL PROCESS TABLE is refused. The probe set is the programs whose whole purpose is "is this process still alive" — pgrep, pkill, ps, jobs, and kill -0, which is the same question spelled as a signal.
**AND THERE IS A SECOND FAMILY, WHICH THIS ROW WAS FILED WITHOUT — **CLOUD-489 ALREADY HAD IT. Absorbed here on 2026-09-02 rather than left in a Backlog row this one would have silently superseded.
CLOUD-489 (filed 2026-08-12, three weeks before this row) specifies family 1 in the same words and adds one this row's probe set cannot reach: a loop whose condition reads a path under the harness's own task-output directory.
until grep -q 'exited with code' "$TASK_OUTPUT"; do sleep 30; done
Measured ~30 times in one session, against that session's own backgrounded tasks. It touches no process table, so every probe named above is blind to it — and it is the same defect: the harness's own record of a task the harness already notifies about. A task-output path is not external state.
So the predicate has two arms, and the second is not optional: **a loop condition reading a $CLAUDE_*-rooted or **/tmp/claude-*/**/tasks/*.output path is refused on the same ground. Judged after heredoc and quote scrubbing, so the same tokens in a fixture or a commit message are not matched.
The boundary that keeps this narrow, and it is a test in both directions: until grep -q … ./build.log against an ordinary project file stays clean. Family 2 keys on the harness's task-output location, never on grep in a loop.
It resolves to a command and an exit code over input.call.segments, which the boundary already has, so it is a gate rather than a judgement (non-negotiable rule 3). Pointer-only: a count, never the pattern or the pid — a probe's operand carries this consumer's task names and paths.
The remedy the deny names is to DELETE THE LOOP, not to fix it. A backgrounded task wakes its caller on exit; the turn in between is the designed state rather than one to fill.
What stays exempt, so this narrows the form rather than closing it: any condition the harness does not report. until curl -sf $URL, a loop over a forge status, a wait on a file another machine writes — none reaches a process table and none is refused.
Ready
- **Authority boundary (§1). **
policy/run-shape.regoplus a compiled-binary tier incrates/batten/tests/it/. No change to whatrun-shape-guard's surviving families decide. - Computable predicate (§2). Two arms, both over a mediated call carrying a
while/untilkeyword. Family 1: a segment whose effective program ispgrep/pkill/ps/jobs, orkill -0. **Family 2 (from **CLOUD-489): a loop condition reading a harness task-output path. Either is refused. Count-only in the finding. - Could-not-look (§2). A call the tokenizer cannot segment is not judged. Absence of a loop is not a finding.
- Deliberately not in scope (§2). General loop-termination analysis. The self-match sub-case as a rule of its own — it is subsumed, and refusing it alone is the wrong fix (see above). Waits on conditions the harness does not report, which stay admitted.
- **Effect (§3). **
read, on the mediated path. - Output and exit (§5). Pointer-only: the offending pattern and the segment index, never the command line, which carries this consumer's directory layout.
- **Commit / bump (§6). **
feat(policy)— patch until 0.1.0. - Test obligation (§7). Over the compiled binary. Shown able to fail, four discriminating observations: the measured
until ! pgrep -f …; do sleep 20; doneis refused; a bracketed pattern ([m]ise) is refused TOO, which is the case that distinguishes this rule from the wrong one — the fix for a self-match must not be an exit from this gate; the anti-vacuity mirror —until curl -sf $URL; do sleep 5; doneis clean, without which the arm is satisfied by a check that refuses every wait; and a call with no loop raises nothing. - Blockers (§8). None.
relatedToCLOUD-489 — the row this one duplicates and now subsumes, whose family 2 is absorbed above; it is the survivor's obligation to carry both arms, and CLOUD-489 should close as a duplicate of this row rather than stay open specifying half the same gate. AlsorelatedToCLOUD-821 (the measured 490-wakeups finding this is the unbounded cousin of), CLOUD-1163 (which retiredrun-shape-guard.shontopolicy/run-shape.rego), CLOUD-251 (the vacuous-pass shape: a condition that cannot hold and a condition that has not yet held are indistinguishable from outside).
Carry this one-line repair, since this row opens the file anyway
policy/run-shape.rego's own header makes three claims that are all false as of 2026-09-02, and they are the same class this row's title indicts — prose asserting a mechanism that is not there:
THE BASH STILL RUNS, and that is the ratchet rather than an oversight. …
run-shape-guard.shkeeps a fourth family (cargo-substitutes-for-a-task) whose blocker is CLOUD-856. So the guard cannot lose these three until it can lose all four, and both authorities decide them until it does. CLOUD-1108 owns that gap…
Measured: mise-tasks/run-shape-guard* does not exist, git ls-files matches no such path, and the fourth family lives in policy/task-substitution.rego. The bash is gone, there is no second authority, and CLOUD-1108 owns nothing here.
A reader trusting that header believes a guard is double-decided when it is not — and this row's whole finding is what happens when an agent trusts a header like that. Fix it in the same commit rather than filing about it; a repair in hand beats a row.
Acceptance
- The measured command above is refused, reporting a count and not the pattern.
- The same command with a bracketed pattern is refused as well.
- A wait on a condition the harness does not report is clean.
- AGENTS.md's "refused by
run-shape-guard" becomes true, and nountil <test>over a non-process condition in the tree or its suites starts failing.
*Filed from the losing side: found by a human reading *ps, which is not a mechanism.
*Grooming, 2026-09-02: milestone set to Stage 3 — Hook convergence. Family 2 absorbed from *CLOUD-489, which this row was filed without citing. The two rows specify one gate.
CLOUD-1158 `[prune]`'s floors are measured against a test-target count that grows one per retired gate, and nothing reds when that basis moves
Why
Floor requires a measured date and validates it as a calendar date — deliberately, because "a stale measurement reads exactly like a fresh one". That is CLOUD-266's rule, discharged. But a date is not the basis; it is a pointer to a basis nobody wrote down, so nothing can tell whether the world under the number has moved.
It has moved, and it moves on a schedule this repository sets for itself.
Corrected 2026-08-29, same day, before dispatch. Three claims in the first draft were loose. §2 put the comparison in
Floor::validate, which is on the path every mediated tool call pays for — see §2 for the correction and the precedent. The "131 MB" figure below was the rlib, not the linked binary. And the 41 → 110 series compares two counts taken different ways; both are now labelled.
The arithmetic nobody watches
Retained bytes after a perfectly successful prune are keep × stems × size. keep = 2 and size are stable. stems is not:
| distinct integration-test stems, CLOUD-766, 2026-08-20 | ~41 |
crates/batten/tests/*.rs, 2026-08-29 |
110 |
tests/*.bats suites still standing |
147 |
Every one of those .rs files is an independent cargo test target — crates/batten/Cargo.toml sets no autotests = false and declares no [[test]], so cargo discovers all 110 — and each links against the workspace library. Sizes, corrected: 131 MB is the libbatten rlib, not the linked binary. CLOUD-766 measured integration test binaries at 36–100 MB (cli-<hash> at 63 MB and 36 MB); the lib test target on this container is 144–172 MB.
The two counts in the table were taken differently, and the row should not imply one series. ~41 is *stems resident in *deps on a tree where the suite had been built (CLOUD-766, 2026-08-20); 110 is tracked test files (git ls-files, 2026-08-29). Both are test targets in substance, but one counts what was built and the other counts what exists. And the count is not drifting by accident: CLOUD-843's bash retirement adds one per retired gate — .claude/rules/policy-modules.md makes the compiled-binary tier the second, non-optional tier of every retirement, so each of the 147 standing suites owes a crates/batten/tests/*.rs when it goes.
So the floor's basis is being multiplied by the campaign the repository is mid-way through, and the mechanism that would notice is the one thing Floor does not record.
Limit of this reading, stated rather than smoothed over: the container's clone is shallow (50 commits, back to 2026-08-26), so the intermediate points between 41 and 110 were not measurable from it. 41 is CLOUD-766's recorded figure and 110 is git ls-files; the trend between them is inferred from CLOUD-843's mechanism, not sampled.
What that costs today
[prune.warm].measured = "2026-08-22". [prune.cold].measured = "2026-08-29". Both carry multiplier = 1, so each floor is exactly the worst lap observed on the day it was taken — CLOUD-861's follow-up already recorded that the x2 became x1 when the number moved, and that whether that was deliberate is written down nowhere.
A floor taken against a smaller stem count is not merely conservative, it is wrong in the direction that fails silently: the check passes, the build then writes more than the basis anticipated, and the exhaustion arrives as a rustc IO error inside a test run. That is the presentation prune.rs's own header says the refusal exists to prevent.
Why it is worth a row
CLOUD-861 makes the floor ratchet from observation, which handles the number. It does not handle the basis: a ratcheting floor over a growing base climbs until it exceeds the container's allowance, and a floor nothing can satisfy is the failure batten.toml's [prune] comment already names as the one that gets a gate switched off. This is the sensor that makes that visible before it happens, and it is a different predicate from either CLOUD-861's or CLOUD-1157's.
Refinement — Ready (a floor records the basis it was measured against, not only the date)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
-
**Source of truth (§1). **
FloorandFloor::validateincrates/batten/src/prune.rs, and the[prune.warm]/[prune.cold]tables inbatten.toml. The basis is the consumer's — which glob and which count — and the comparison is the engine's, which is what keeps non-negotiable rule 1.mise-tasks/target-prune.shno longer exists (retired whole under CLOUD-1059);mise-tasks/**andtests/**/*.batsare refused byV-SHELL-RULE-EDITEDwith no override and nobypass_env. -
**Computable predicate (§2). **
Floorgains the basis beside the date: a declared glob and the file count it held when the lap was measured, plus a tolerance. The comparison reds when the live count for that glob has moved past the tolerance sincemeasured— a count against a count, an exit code over the tree, no judgement and no model verdict.The comparison must NOT live in
Floor::validate— corrected 2026-08-29 before dispatch, because the first draft put it exactly there.crates/batten/src/config.rs:1038callsprune.validate()from the shared config-load path, which **every **batteninvocation runs,batten hookon every mediated tool call included. A glob walk there is taxed onto thePreToolUsepath againstperf-assert's ceiling, andbatten checkover 654 tracked files already costs ~3.6 ms above the process floor. The repository has ruled on this exact shape once:claim-race-checkwas moved off the mediated call because a round trip on every tool call is disqualifying, andRuleKind::scopespairs every spawning kind withRuleScope::Treealone.So:
Floorcarries the declared basis (it is config, and validating its shape at load is free), and the comparison against the live tree runs inbatten target prune— alreadyCost::EffectonSurface::VerifyOnly, whichperf-assertdeliberately budgets no ceiling for — or as atree-scoped rule row underverify. Either is acceptable;Floor::validateis not. -
Effect (§3). No new verb and no new surface. Shape validation of the declared basis stays at config load; the tree comparison is read-only and runs on
Surface::VerifyOnly, never on the mediated call. -
Output & exit (§5). Pointer-only per non-negotiable rule 4: the glob, the declared count, the live count, the tolerance and the
measureddate. Never a file listing — the count is the finding, and the paths are unbounded. -
**Commit / bump (§6). **
fix(prune)→ patch until 0.1.0. The workspace is 0.0.130, so release-plz bumps the patch whatever the type says. -
**Test obligation (§7). **
crates/batten/tests/target_prune.rs, the compiled-binary tier, since the live count has to come from a real tree rather than from a fabricated one. Shown able to fail (CLOUD-418):- a floor whose declared basis count no longer matches the fixture tree is refused by the verify-surface comparison, naming both numbers;
- the placement case: a mediated call (
batten hook) over a tree whose basis count has moved is not refused and does not walk the tree — red if the comparison is put back on the config-load path, which is what the first draft specified; - anti-vacuity twin: a count inside the tolerance loads clean, and a tree at exactly the declared count loads clean — without these the check is "always red", which is a gate nobody keeps;
- a floor declaring no basis at all is refused, on the same ground
measuredalready is: an absent basis reads exactly like a satisfied one.
-
Blockers (§8). None. It composes with CLOUD-861 and CLOUD-1157 and depends on neither: the number, the reclaim and the basis are three separate mechanisms, and this one is landable alone.
relatedToCLOUD-861 (the ratcheting floor this keeps satisfiable), CLOUD-1157 (the reclaim that sets whatsize × stemsactually retains), CLOUD-266 (the budget-carries-its-measurement rule this extends), CLOUD-843 (the campaign moving the term).
Acceptance
- Each floor in
[prune]declares the basis it was measured against — a glob and a count — beside its date. - A tree whose live count for that glob has moved past the declared tolerance is refused on the verify surface, naming the declared count, the live count and the date.
batten hookdoes not walk the tree for this, andperf-assertis unmoved — the comparison is off the mediated call.- A tree inside the tolerance loads clean, so the check discriminates rather than always refusing.
- The declared basis is the consumer's, in
batten.toml; no count and no glob appears incrates/batten.
Provenance. Found while diagnosing why mise run land still exhausts the disk when it laps, with batten target prune in place and released in v0.0.129. The reading that produced it: keep = 2 bounds copies per stem and nothing bounds stems, and the repository is actively multiplying stems as a matter of policy.
CLOUD-760 Effect-class facts: read a delegated analyser's structured output, not just its exit bit
The mechanism CLOUD-690 is missing, and the one CLOUD-756 concluded is required.
CLOUD-756 landed the principle: "Batten must not COMPUTE symbol resolution. It should CONSUME resolved facts — and an exit code is one bit, not resolved facts." This is the fact that makes it real.
What is thrown away today
Batten already runs delegated analysers and already reads their output — but only two ways, both lossy:
commandrule kind (rules.rs:2223): streams nulled, exit code only. One bit.exec_pattern(outputs::hits,exec.rs:1479): a literal substring over captured child streams, deliberately not regex.
So the limitation was never "Batten cannot read a delegated tool's output" — it is "Batten can only read it as text or as one bit." CLOUD-690 names this as a capability gap; this is its fact-model half.
cargo clippy --message-format=json emits spans, resolved paths and lint names. Those are resolved facts, produced by the compiler's own name resolution, and Batten discards all of them to look at $?.
Why this is the highest-value class
Three of this crate's load-bearing architectural policies are hand-rolled #[cfg(test)] source scans because no rule kind can see symbols (CLOUD-756): no_second_repo_root_resolver_exists, no_second_git_invoker_exists, no_ancestry_decides_merged_ness. CLOUD-359's use-graph rule is a fourth. Each is a symbol question, and each is blocked on there being no fact for it.
The worked failure is on record: a grep for Command::new reported 14 spawn sites where there are 9, because clap::Command and std::process::Command share a spelling. Bytes cannot separate them; resolved paths can, trivially.
Cost class: effect — and this is where §5 gets decided
Computing this fact spawns a process. That is the whole reason it is its own class:
batten checkisEffect::Readandrun_static(rules.rs:1544) refuses any spawning kind outright, exit 1. If an effect-class fact were resolvable undercheck, that promise weakens.- The honest options are (a) effect-class facts are available under
enforceonly, mirroring how spawning kinds are already confined; (b)checkgains a way to consume a fact resolved elsewhere without resolving it itself — a cached or receipt-backed fact, which is a shape this repo already has machinery for. - (b) is worth taking seriously rather than dismissed:
verifyalready writes SHA-keyed receipts thathookreads (CLOUD-203). A fact resolved once atverifytime and read as data later is exactly the amortisation this milestone is about, and it keepscheckstructurally read-only.
What this issue lands
- A fact whose value is a delegated analyser's structured output, with its provenance (which tool, which version, which invocation) — because a fact whose meaning depends on an unrecorded tool version is not canonical.
- The §5 decision above, stated rather than implied.
- Determinism handling: structured diagnostics carry absolute paths and vary by tool version.
identity::canonical_repo_pathis the existing answer for the path half; the version half needs the provenance above. Without both, §6 byte-stability does not hold.
Not in scope: a rule kind that consumes it, and any per-language analyser Batten would maintain. This is the fact and its boundary. Whether the first consumer is the spawn census (CLOUD-743) or the use graph (CLOUD-359) is downstream.
Acceptance sketch (not yet a Ready block)
- A structured fact from clippy is available to a rule, carrying resolved paths rather than source text.
- The
clap::Commandvsstd::process::Commandcase is the pinned test — a fact-backed count returns 9, and the byte-backed count that returns 14 is what it replaces. - Pointer-only survives: diagnostics carry spans and messages, and none of that reaches output. A finding reports
path:lineand a rule id. - Two runs over identical bytes with an identical tool version produce identical output; a differing tool version is visible in the fact, not silently absorbed.
batten checkeither still refuses to resolve it, or consumes a receipt-backed one — and whichever holds is asserted, not assumed.
Filed from a subprocess-boundary audit whose own miscount is the worked example.
RE-FOUNDED 2026-08-22 — the prior art this row did not cite
⚠️ Corrected the same day, hours later. The premise was NOT false, and this header said it was.An earlier revision of this section (mine) opened "the premise above is false" and struck through the row's own sentence "Batten can only read it as text or as one bit." That retraction was wrong and is itself retracted.
secrets.rsreads ripsecrets' output as colon-delimited text —parse_line(:1286-1307) issplit_once(':'), anumber.parse(), and a longest-prefix path match, andSCANNER_FLAGS = ["--only-matching"](:103) exists precisely so the third text field is the matched literal. ripsecrets performs no name resolution at all. So the row's original sentence describes that module exactly, and the sentence stands.What the measurement below actually establishes is prior art for how to adopt a delegated analyser, not a counterexample to what is missing. The distinction matters because it is the row's whole thesis: text is not resolved facts, and
--message-format=jsonfrom clippy is the first thing in this crate that would be.The failure was reading a subagent's summary of
secrets.rs("parses structured output, fail-closed") as a claim about structure when it was a claim about parsing, and overturning a correct sentence on it without opening the file. The section below is kept because its content is useful; only its verdict is withdrawn.
crates/batten/src/secrets.rs already reads a delegated analyser's structured output, and reconciles it against the exit code fail-closed. Measured against the tree:
| what | where |
|---|---|
pinned binary, installed through PROVISION_VERB |
secrets.rs:89, :91 |
flags pinned beside the parser — ["--only-matching"] |
secrets.rs:103 |
| exit codes pinned and cross-checked | secrets.rs:105 |
the adapter — struct Match { path, line, span: SecretSpan } |
secrets.rs:1017 |
parse_line, wrapping each span into an opaque identity::SecretSpan at the pipe boundary so no matched byte survives as a &str |
secrets.rs:1013-1016 |
| fail-closed on every exit/parse disagreement — unparseable line, clean-exit-with-matches, found-exit-with-no-matches, any other code, a signal | secrets.rs:1222-1268 |
RuleKind::Secrets's own doc states the contract this row thought was missing: a command rule "yields one batch verdict per glob — no per-secret path:line, no per-secret identity, and nothing to key. Reading the scanner's output is precisely what makes this kind different."
So the gap is not the capability. It is that the capability exists once, hard-wired to one scanner, and is not a fact. That is a narrower and more tractable row than the one filed, and it changes where the work goes: this is not inventing structured delegation, it is generalising an existing, tested, fail-closed instance of it. mem:prior-art-and-issue-hygiene — mine it, don't mirror it. The invariant to carry across verbatim is secrets.rs's own: clean is never inferred from a stream that failed to parse.
What actually is missing: nothing occupies Cost::Effect
CLOUD-757 landed and is Done. facts.rs carries Cost (Free/Read/Effect/Stateful, :99), Surface (Hook/Check/VerifyOnly, :156), Class as their product with meet on both axes (:216), Look<T> (:256) and Fact with ten variants (:295-326, ALL at :453).
Every one of the ten is Free/Read × Hook/Check. No fact is Cost::Effect. No fact is Surface::VerifyOnly. Those variants were declared unoccupied on purpose — facts.rs:161-162: "The boundary this names does not exist yet … naming it is what keeps the first fact that needs it from inventing its own."
This row is that first fact. That is a sharper §1 and §3 than anything in the original body, and it makes the §5 argument above concrete rather than open: the question is not whether an effect-class fact is admissible, it is what the first occupant of a reserved variant owes.
Two corrections to the body above
The count is wrong. "Three of this crate's load-bearing architectural policies are hand-rolled" — measured 2026-08-22, there are seven, all fs::read_to_string + source.contains over crate_sources(skip_self) (git.rs:2395), needles assembled by [..].concat(): no_second_repo_root_resolver_exists (:2334), gix_is_confined_to_this_module (:2634), no_second_git_invoker_exists (:2690), no_module_assembles_its_own_git_argv (:2756), no_ancestry_decides_merged_ness (:2416), no_gix_gap_primitive_survives (:2656), every_stays_shelled_out_claim_names_its_price (:2711). And the census half already migrated: clippy.toml:35-39 denies std::process::Command at deny in Cargo.toml:56, meta-gated by crates/batten/tests/spawn_census.rs.
The vocabulary is pre-Rego. The body asks for "a rule kind whose predicate is over a delegated analyser's structured output" and reasons in [[rule]] rows. The engine has since grown RuleKind::Policy (rules.rs:253), which evaluates registered Rego modules over the resolved fact set, and the projection of that set into the policy input is CLOUD-834's. So the consumer of this fact is a Rego module rather than a new kind — which also deletes "a rule kind that consumes it" from the not-in-scope list, because no such kind needs to exist.
Refinement — Ready (2026-08-22, against the re-founding above)
Refinement gate: Definition of Ready & Done. This block carries only specializations.
- Source of truth (§1).
crates/batten/src/facts.rs— a newFactvariant, the first occupant ofCost::Effect, with itsclass()const beside the existing ten and itstree_key()in the same table. Appended, never inserted. The acquisition generalisessecrets.rs's adapter shape — pinned binary, flags pinned beside the parser, exit code reconciled against the parse — and that module stays the one authority on that shape rather than gaining a second copy. - Computable predicate (§2). A registered Rego module over the projected fact counts the crate's
std::process::Commandsites and returns 9, not the 14 a byte scan returns and not the 11 a syntax matcher returns. The three numbers are the discriminator: a fact that cannot separateclap::Commandfromstd::process::Commandhas resolved nothing, andsurface.rs's bareuse clap::{…, Command}import is what makes the case real rather than hypothetical. - Effect (§3).
Effect × Check— the class is the whole point of the row, so it is stated and exhaustively matched, never inferred.Cost::Effectmeans resolving it spawns a program and means only that (facts.rs:96).Surface::Hookis refused:run_staticalready refuses a spawning kind outright, and this fact must not weaken that promise. Whethercheckmay resolve it directly or must consume a receipt-backed one is decided in this row and asserted by test, never left to a comment. - Generated artifacts (§4).
schema/batten.schema.jsonandschema/batten.local.schema.jsonregenerate for any declaration key the fact adds. Regenerate withmise run fix; never hand-merge a generated diff.derived-checkandschema-checkgate both. - Output & exit (§5). Pointer-only, non-negotiable rule 4, and the analyser's diagnostics are exactly the payload that must not escape: they carry spans, messages and absolute paths. A finding reports
path:lineand a rule id.identity::canonical_repo_path(identity.rs:368) answers the absolute-path half of determinism; the tool version is carried in the fact as provenance, because a fact whose meaning depends on an unrecorded tool version is not canonical and §6 byte-stability does not hold without it. No new verb and no change to the exit table. - Commit / bump (§6).
feat(facts)— patch until0.1.0. Not breaking for the library surface: the variant is appended and no existingFactarm moves, which is whatmise run semvermeasures. - Test obligation (§7). Shown able to fail:
- (a) the spawn-site count returns 9 over this tree, and
surface.rs's clap sites are excluded — asserted, since that pair is the entire reason the fact exists; - (b) two runs over identical bytes with an identical tool version produce byte-identical output;
- (c) a differing tool version is visible in the fact, not silently absorbed — the case a determinism test alone cannot see;
- (d) an unparseable stream yields
Look::CouldNotLook, asserted distinct from "no findings", and an exit code disagreeing with the parse is fail-closed —secrets.rs:1222-1268's five cases, carried rather than re-derived; - (e) the surface bound holds: the fact is unresolvable on
Surface::Hook, asserted by the exhaustive-match disciplinecrates/batten/tests/facts.rsalready applies to the ten variants.
- (a) the spawn-site count returns 9 over this tree, and
- Blockers (§8). None. CLOUD-757 is Done, and the relation this row carried has been dropped rather than left to read as live.
blocksCLOUD-762 and CLOUD-914.relatedTounchanged.
Acceptance, restated against the re-founding
- A
Factvariant occupiesCost::Effect, and it is the first — asserted, so the reserved variant stops being empty for a stated reason. - A Rego module over it counts 9 spawn sites; the 14 and the 11 are both recorded beside it as what it replaces.
- Provenance — which tool, which version, which invocation — is part of the fact.
- An unparseable stream is
CouldNotLook, and exit-versus-parse disagreement is fail-closed, both asserted. Surface::Hookcannot resolve it, asserted rather than intended.
Two smaller corrections from the same verification pass, 2026-08-22
§8's blocks list is now wrong. It reads "blocks CLOUD-762 and CLOUD-914"; both relations were removed later the same day. CLOUD-762's measurement deliverable needs nothing unbuilt, and CLOUD-914 turned out to be a syntax question (Read × Check) rather than a name-resolution one. CLOUD-756 also dropped its blockedBy on this row — its §2 migration reaches the current scan's fidelity on Fact::Lines alone. So this row now blocks nothing, which is a fact about how much was wrongly parked behind it rather than about its value: it is still the only route to resolved-symbol facts, and the clap::Command 14-versus-9 case is still the pinned test that only it can pass.
What survives untouched, and it is the substance. Cost::Effect, Cost::Stateful and Surface::VerifyOnly are declared and unoccupied — verified by direct read of facts.rs, where the ten class consts (:332 BYPASS through :449 PROSPECTIVE) are every one Free/Read × Hook/Check. facts.rs:161-162 reserves the surface variant so "the first fact that needs it" does not invent its own boundary. This row is that first fact. That framing was reached in the same pass that got the premise wrong, and unlike the premise it checks out.
Every file:line pointer in this row's "what is thrown away today" table is wrong — re-resolved 2026-08-22
The verification pass read each cited location instead of trusting it. Six of the pointers in this body do not resolve to what they claim, and the three in the opening table are the load-bearing ones, because they are the row's evidence that the capability is missing.
| the body says | what is actually there | the correct pointer |
|---|---|---|
rules.rs:2223 — the command kind streams nulled |
a comment about retires_with and its base requirement |
the stream nulling is rules.rs:4580-4581 |
exec.rs:1479 — outputs::hits, a literal substring over child streams |
an #[expect(clippy::disallowed_types)] region, not the function |
outputs.rs:208 — and note the module was wrong, not just the line |
rules.rs:1544 — run_static refuses any spawning kind |
unrelated | run_static is declared at rules.rs:2928 |
secrets.rs:1013-1016 — parse_line wrapping each span |
scan's doc comment |
parse_line is at secrets.rs:1286-1307; struct Match at :1017 |
secrets.rs:1222-1268 — the five fail-closed cases |
unrelated | the parse boundary and cross-check begin at secrets.rs:1445; scan at :1035 |
identity.rs:363 — canonical_repo_path |
unrelated | identity.rs:368 — corrected in §5 above |
What does resolve, checked in the same pass: rules.rs:253 (Policy,), secrets.rs:89 (SCANNER), :103 (SCANNER_FLAGS), :105 (the exit-code table's doc), clippy.toml:35-39 and Cargo.toml:56 (disallowed_types = "deny"), facts.rs's ten class consts at :332–:449. And facts.rs:96 for the Cost::Effect doc is wrong — it is at :106.
The substance is untouched. The command kind really does null its streams; outputs::hits really is a literal substring; run_static really does refuse a spawning kind; secrets.rs really is fail-closed on every exit-versus-parse disagreement. Every claim this row makes holds. Only the addresses were stale — three of them from the original filing two days ago, three from a correction section written this morning.
Which is exactly the decay CLOUD-756's own follow-up names: "the test NAMES are the durable key; the line numbers are a measurement taken on one commit and nothing keeps them true." That row said it about a table it had just rewritten. This row is the proof it was right, at a rate of six wrong pointers in two days across one body. An implementer should resolve every symbol name in this row against the tree and ignore every number in it, including the ones this table just corrected — they were true at 170c7c4 and nothing keeps them true either.
No gate sees this. ready-cites-check judges backticked paths containing a slash; file.rs:NNN has none, so the whole class is invisible to every gate in the repo — which is why six of them accumulated in one body without a single refusal.
Refinement — evidence plan
- Unresolved decision: whether
checkresolves thisEffect × Checkfact directly or consumes a receipt-backed fact. - Probe: generalize the recorded
secrets.rsparse/exit reconciliation shape for clippy structured output and run the stated spawn census over the existing tree, includingsurface.rs'sclap::Commandimport. - Record: resolved count 9 versus prior 14/11 counts, tool/version/invocation provenance, identical-byte repeat output, changed-version visibility, parse/exit disagreement behavior, and Hook refusal.
- Ready when: the §5 boundary is selected and asserted, and every listed acceptance case passes without analyser diagnostics escaping pointer-only output.
CLOUD-858 Three Todo rows carry no readable Ready block, so `graph-check` refuses the whole board and the ready frontier cannot be computed at all
All three rows are repaired, 2026-08-22. The closure check is NOT run, and that is the honest remaining half.
- CLOUD-472 — out of Todo. Its four open questions are unanswered design decisions, so the questions-are-artifacts protocol applies exactly as this row predicted. Nothing in its body changed: it was already honest, and the column was the defect. The reason is recorded on the row.
- CLOUD-275 — Ready block written,
ready-lintexit 0. Its mechanism was already specified and its preconditions are realblockedByrelations rather than open questions, so it could take a genuine block and stay in the queue. §2 makes the degraded case the discriminating one: a host exposing no identity yields the fields valuedunknown, never absent.- CLOUD-134 — dialect converged AND out of Todo. Converging alone would have been wrong. Its own Decision section says "Gate on a named first consumer being concrete" and no consumer is named, so a passing block would have papered over an unmet precondition — which this row's last acceptance bullet forbids. It now carries a real
**Refinement — Ready**block that declares the open question, and sits in Backlog. The retiredfeat→ minor arrow is corrected to patch-until-0.1.0, as this row's third acceptance bullet asked.What is not verified:
mise run graph-checkover the full Todo closure. It needs every Todo payload, and a hand-assembled board payload is the forgeable-verdict shape this repository already measured —ready-lintgreen three times against text in a local file, once under an id no row carried. The per-row half is verified against the tracker's own responses; the closure half needsmise run board-payloads.Two Todo rows outside this row's scope were also unmilestoned and would have failed
graph-check'stodo-unmilestonedclause independently: CLOUD-924 and CLOUD-925, both filed 2026-08-22, both now on Stage 2. A closure run before that fix would have refused for a reason this row does not name.
Why
CLOUD-595's sweep ran ready-lint over every row in the Todo column on 2026-08-21 — 64 rows, each payload recovered byte-identical from get_issue via mise run board-payloads rather than re-typed. 61 exit 0. Three exit 1, and none of the three is a §6 arrow:
| row | verdict |
|---|---|
| CLOUD-472 | CLOUD-472:0 no-ready-block |
| CLOUD-275 | CLOUD-275:0 no-ready-block |
| CLOUD-134 | CLOUD-134:16 non-canonical-ready-opener (use **Refinement — Ready) + CLOUD-134:16 ready-block-without-clauses |
The two §6 defects the sweep found — CLOUD-854's unqualified-break-claim and CLOUD-464's bump-disagrees-with-type (test implies no bump) — were repaired in place, because a §6 line is a mechanical edit that moves no scope. These three are not. Repairing them means writing a Ready block, which is a decision about what each row is for.
Why this is a defect rather than the board being honest
graph-check's todo-not-ready clause (CLOUD-375, Done) runs ready-lint over every piped Todo payload and refuses on exit 1. So while these three sit in Todo, graph-check exits non-zero over any closure containing them — and the ready frontier it emits is the thing mem:workflow/agent-fanout says "every session computes" rather than a dispatcher. One unrefined row does not merely misreport itself; it makes the whole board unjudgeable, which is CLOUD-595's finding one level up and the reason that row exists.
Each of the three is a different repair, which is why they cannot be swept as one:
- CLOUD-472 carries
**Open questions blocking Ready:**and four numbered questions, and no Ready block at all. The questions-are-artifacts protocol says such a row stays out of the ready queue — so the honest repair is probably the column, not the body. That is a state transition, andmem:workflow/board-statesclassifies a transition as permission-bearing where a content edit is not. - CLOUD-275 has an acceptance list and no Ready block. It needs refining, not moving.
- CLOUD-134 opens
**Definition of ready (computable predicate)**— the dialect CLOUD-299 recognised only in order to report — and carries no(§N)clause label, so it clears the opener and fails the clause floor. Its**Commit type / bump:**feat→ minoris also a retired arrow, andready-lintcannot see it because the label is notCommit / bump (§6). Converging the dialect is what would make that visible.
What was deliberately not done
Not repaired under CLOUD-595. That row's §1 states "No issue's §1–§5, §7 or §8 content is an input to this pass or an output of it", and its §3 states the pass is "tracker content only … so nothing here is a permission-bearing act". Writing three Ready blocks and moving a column are both outside that, and narrowing a sibling row's scope to fit is the shape this repository refuses.
Refinement — Ready (2026-08-21)
- Source of truth (§1). The three rows themselves — each one's own body and its own column on the tracker. No repository artifact is an input to this or an output of it, and no gate changes: every mechanism this needs (
ready-lint,graph-check'stodo-not-ready) is already landed. What is missing is refinement, not machinery. - Predicate (§2).
mise run graph-check, piped the payloads for the fullTodoclosure, exits 0. Equivalently and per row:mise run ready-lintexits 0 for each of the three, OR the row is no longer inTodo. A command and an exit code over payloads the tracker returned; nothing here is judged by reading. - Effect (§3).
readwith respect to this repository — no command surface moves, no file is added, no allowlist changes. The writes are tracker writes, and one of them (CLOUD-472's column) is a state transition, whichmem:workflow/board-statestreats as permission-bearing where a content edit is not. - Output and exit (§5). Unchanged.
graph-checkalready emits the ready frontier and a WIP count on stdout and refuses pointer-only; this row changes what that command ANSWERS, never what it prints. - Commit / bump (§6). No bump. This row lands no commit against the crate at all — it is tracker content. Were a follow-up to touch the tree, it would be
chore(board), which never releases, so the arrow is the same either way. - Test obligation (§7). Two checks, both over payloads fetched with
get_issueand recovered byte-identical bymise run board-payloads, never re-typed:mise run ready-lintexits 0 for CLOUD-275 and CLOUD-134, and for CLOUD-472 either exits 0 or the row is out ofTodo.mise run graph-checkover the wholeTodoclosure exits 0 — the check that actually matters, because the refusal this row is about is a property of the closure rather than of any one member.
- Blockers (§8). None. Every gate this rests on is landed, and the three repairs are independent of each other.
Acceptance
- Each of the three rows either carries a Ready block that
ready-lintexits 0 on, or is out of theTodocolumn — decided per row, with the reason recorded on that row. mise run graph-checkover the fullTodoclosure exits 0, which it cannot today.- CLOUD-134's
feat → minorarrow is corrected in whichever direction its dialect converges, since converging the opener is what makes it readable at all. - No row's scope moves to satisfy the gate: a row that genuinely has open questions leaves the ready queue rather than acquiring a Ready block that papers over them.
Found by CLOUD-595's sweep — by running the predicate over the whole column rather than over the three rows the incident named.
CLOUD-135 Define the judge's payload-privacy boundary (what may be sent to a model)
Why
Batten's output law reduces content to pointers (CLOUD-92); the judge (CLOUD-56) is the one component built to send repo content to a model. Ungoverned, that is an egress path inside the tool whose purpose is keeping content out of model context. The bound is set by what the egress buys: the judge's verdict is advisory-only and structurally unable to block (decision, 2026-08-07 evidence base), so no payload is justified by enforcement power — the boundary defaults to refuse, and every byte that crosses must be explicitly admitted, capped, and recorded. This issue closes the previously open positions (classes, redaction, local models, protected-content default) as decisions carried by a computable mechanism: a payload-assembly module that is the only way a judge payload can exist.
Decisions
- Closed payload-class taxonomy. A judge payload is built from exactly three classes: rule (the judge row's own committed
idandcriteriatext frombatten.toml), content (bytes of working-tree files matched by the row's glob), and pointer (path:linespans, counts, SHA-256 hashes). The constructor's input types admit nothing else — no environment values, no transcript text, no findings-store content, no config beyond the row, no git history. Exclusion is structural, not filtered: the module exposes no API that accepts arbitrary bytes into a payload. - Protected bytes never cross. A matched file that is a member of the resolved
protectedset (PathSet::contains— the same membershipbatten hook's protected-path gate uses) refuses the whole invocation as a usage error (exit 1 at the caller), naming the rule id and the protected-match count. - No local-model carve-out. Whether a configured command reaches the network is not computable from config, so the boundary is uniform over every judge command; locality is the operator's choice of
run, invisible to the boundary. - Cap and channel. Per-row
max_payload_bytes, engine default 16 384 (the precedent of the argv-batching boundMAX_FILES_BYTES), tighten-only under the §8 raise-only clamp — abatten.local.tomlmay lower it, never raise it. Over cap refuses whole, never truncates. The payload crosses on the judge command's stdin — never argv (world-readable process state), never a temp file. - Pointer-only invocation record. Every assembly — refused or crossed — yields a byte-stable record: rule id, payload byte count, payload SHA-256, matched-file count, disposition. The record type and its serialization are defined here; CLOUD-56 registers it through the findings store. Stdout stays silent on a clean run (§6).
Rejected alternatives
- Redact-and-send for protected content — a redaction derives from the protected bytes, and no computable check certifies what it leaks; against an advisory-only verdict the residual risk buys nothing. Refusal is the only verifiable posture.
- A committed opt-in key for protected egress — a widening surface (§8 keeps configuration narrow) that purchases no enforcement power. If a consumer ever needs it, that is a new recorded decision, not a latent key.
- A local-model exemption — an exemption keyed on a property the engine cannot check is policy by assertion.
- Truncate-to-cap — a truncated payload silently judges different content than the row named.
Definition of done
- A payload-assembly module in
crates/batten: the single constructor of judge payloads, typed to the three classes. - Assembly-time admission: exact per-path membership of every matched file against the resolved
protectedset, checked before any file content is read; one protected member refuses the invocation with a pointer-only diagnostic. max_payload_bytessemantics per judge row: default 16 384, layered tighten-only; over-cap refuses whole. (The key's schema serialization lands with CLOUD-56's row shape; the semantics and enforcement live here.)- The invocation-record type with byte-stable serialization, pointer-only fields.
- Module fixtures assert the sentinel property: content planted in an environment variable, an unmatched file, and a protected file never appears in an assembled payload.
Acceptance (fixture-level, each falsifiable)
- A fixture tree where the row's glob matches one protected file: assembly refuses; the diagnostic carries the rule id and count
1; no payload value exists. - A clean fixture: the assembled payload byte-scans positive for the row's
criteriaand the matched file's bytes, and negative for three planted sentinels (environment variable, unmatched file, protected file). - A payload exactly at
max_payload_bytesassembles; one byte over refuses whole. - A lowered cap applies; a raised cap is refused under the raise-only clamp.
- Two assemblies of the same fixture yield byte-identical payloads and byte-identical records.
- Record serialization byte-scans negative for the same sentinels and for matched-file content — the record never carries payload bytes.
- No consumer identifier enters
crates/batten(rule 1).
Refinement — Ready (three admissible payload classes by type; protected membership refuses whole; caps tighten-only; stdin channel; pointer-only byte-stable record)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Source of truth (§1). The payload-boundary module in
crates/batten. Theprotectedset stays the consumer's committed list inbatten.toml(already resolved bySets::from_config); this issue adds no second copy of it. - Computable predicate (§2). Not expressible as a
batten.tomlrule: it is an engine capability — the capability gap CLOUD-56 links as its blocker, which is how this lands in the engine rather than the bash layer. The predicates are exact: class admission by constructor input type, protected admission byPathSet::containsper matched path, cap admission by byte comparison. No inference anywhere. The gate is the fixture suite inmise run test:cargo, an hkgatestep run bymise run ci. - Effect (§3). No new command; no effect-table change. Assembly is internal to the
enforce-only judge path; its refusals surface as usage errors (exit 1) through the caller. - Output & exit (§5). Diagnostics and the invocation record are pointer-only (rule id, counts, hash) and byte-stable; refusal is exit 1 at the caller — a statement about the invocation, not a policy verdict — and nothing here produces exit 2.
- Commit / bump (§6).
feat→ patch until 0.1.0. - Test obligation (§7). Module-level fixture tests (the Acceptance list) land here. The module is unreachable from any verb until CLOUD-56 wires it, so the binary-level assertions — protected-path refusal surfacing as exit 1 from
batten enforce, and no payload bytes in any engine output — are enumerated in CLOUD-56's acceptance and land with it; naming them in both places is what keeps neither issue landing without the other. - Blockers (§8). None — implementable now. This issue blocks CLOUD-56: no payload may be assembled before what-may-cross is bounded.
relatedToCLOUD-92 (the pointer-only output law, extended here to the input direction), CLOUD-93 (the gate/judge line), CLOUD-82 (the drain budget the record renders under), CLOUD-133 (guard-decision telemetry: sibling record vocabulary, kept distinct).
Stated assumptions
- The taxonomy assumes the v1 judge domain is glob-matched working-tree files (CLOUD-56). A transcript-scoped judge would need a new class decision — a new issue, not a widening here.
- SHA-256 is pinned for record byte-stability; the property needed is collision resistance, not the specific algorithm.
- The record registers through the findings store once CLOUD-56 wires it; until then the type and serialization are the artifact.
CLOUD-1310 A declared exemption whose owning issue has CLOSED is unrefusable from a tree-scoped module — `hooks-wiring-check`'s closed-owner rule retired with no successor, and the permanent-exemption shape it existed to catch is now uncaught
Why
mise-tasks/hooks-wiring-check.sh:298-316 carried a rule with no successor, and CLOUD-1160 retired it as a withdrawn: arm. This row is the durable home that arm claims.
The rule: a DECLARED exemption row naming an owning issue that is closed is itself a violation.
closed_owners=$(jq -rs '[ .. | objects
| select(has("id") and has("statusType"))
| select(.statusType == "completed" or .statusType == "canceled")
| .id ] | unique | .[]' <<<"$payloads")Its own comment states the class exactly: "A declared row whose owner is a CLOSED issue is the permanent-exemption shape the DECLARED pattern exists to refuse: the retirement's licence outlives the row that was supposed to deliver it."
**Two directions survived the port and this one did not. **policy/harness-wiring.rego keeps unowned (a row naming no issue) and stale (a row matching nothing wired). Between them a table cannot grow an anonymous row or keep a spent one. What it CAN now do is keep a row whose issue was closed years ago — a licence with an owner who has stopped owning it, which is the exact failure mode the table's whole design is aimed at.
Why it is unspellable, not merely unported
The shell decided it from get_issue payloads the caller piped in — the agents-fetch-gates-decide pattern, because no gate path carries a tracker credential. A scope = "tree" policy module has no stdin, and no tree fact carries a tracker row's status:
input.tree.recordsis the[[recorder]]channel and reachesinput.factson the mediated surface, which a tree-scoped rule never sees.batten.toml'sissue-readreceipt already records{id} {updatedAt} {now} {digest:description} {slug:status}— the status is captured — but it is areceiptrow, read at the hook boundary, not projected onto the tree document.input.tree.capturedis a declared reduction (present,count, a bounded token) over the capture store, by design; a status slug per issue id is not a reduction it expresses.
So the fact exists in the store and has no route to a tree-scoped predicate. That is the gap, and it is one route rather than a fact family.
What it would take
A declared projection of the issue-read receipt onto the tree surface, keyed by issue id and carrying the status slug only — never the body, never the title. A module then reads the ids its own exemption table names and refuses those whose status is a closed type.
Note the same staleness question input.tree["tool-verdict"] answers and [[rule.external]] does not: a status read a week ago is not evidence about now, so whatever channel lands owes a recency bound. The receipt already stores {now}, so the material is there.
Scope beyond this one table, which is why it is worth a row. Every exemption table in this repository keyed to an issue has the same hole — [[waiver]] rows, #MUTANT-EXEMPT directives, the stays-bash: markers, and any future declared table a retirement leaves behind. A closed owner is the generic tell that an exemption has outlived its reason.
Refinement — Ready
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Authority boundary (§1). A declared projection of an already-captured receipt onto the tree document.
kind:mechanism: no verb moves, and the tracker's vocabulary stays inbatten.tomland in consumer modules, never incrates/batten(non-negotiable rule 1). - Computable predicate (§2). A module reads, for an issue id it names, whether that row's status is of a closed type and how old the reading is. A status never captured is could-not-look and refuses nothing.
- Could-not-look is the common case (§2). No tracker credential exists on any gate path, and the store dies with its container, so the ordinary state is "nobody has read this row". That must be distinguishable from "the owner is open" — reading absence as open is the false green the shell avoided by leaving the rule unenforced on absent stdin.
- Output and exit (§5). Pointer-only: the exemption's own subject and the issue key it names, never a title, a body or a status update's prose.
- **Commit / bump (§6). **
feat(facts)→ patch until0.1.0. - Test obligation (§7). Over the compiled binary. Shown able to fail per CLOUD-418: a row whose owner is closed is refused; the same row with an open owner is not (the anti-vacuity mirror); a row whose owner was never captured is could-not-look and refuses nothing; a reading older than the declared bound does not answer.
- Blockers (§8). None.
relatedToCLOUD-1160 (which retired the predicate and owes this row), CLOUD-525 (which added the rule), CLOUD-418, CLOUD-1154 (the tree-surface producer question this is an instance of).
Acceptance
- A tree-scoped module can decide "this exemption's owner is closed" over a declared, recency-bounded projection.
policy/harness-wiring.regoregains the third direction, so its table cannot keep a row whose owner stopped owning it.- An uncaptured owner refuses nothing, demonstrated rather than asserted.
- No tracker prose reaches any finding, asserted.
Filed while landing CLOUD-1160, whose withdrawn: arm names this row as the class's home. Written because that arm claimed a durable home before one existed.
CLOUD-821 `run-shape-guard` has never been registered, so AGENTS.md's "Gated by `run-shape-guard`" is prose, and its `run_in_background` exemption passes a timer with no exit condition
Why
Landing CLOUD-776 took ~950 tool calls in one session, and almost none of them were work. Measured from that session's own transcript:
| count | |
|---|---|
sleep N; tail <log> calls |
490 |
| background tasks launched | 524 |
| median launch → completion notice | 590s — i.e. exactly the nominal sleep |
| content-free replies to task notifications | 40 |
mise run verify calls |
5 |
| polls that changed a decision | 2 |
The mechanism being hand-rolled already existed and demonstrably fired: 523 of 524 backgrounded tasks re-invoked the session on exit, including every land failure and its final success. Every genuine intervention came from a task exiting, never from a poll. This is the same duplication CLOUD-489 measured, in a shape CLOUD-489's predicate does not reach — see Relation to CLOUD-489 below.
Two defects, one file, one edit site.
Defect A — the guard has never been registered.
git log -S'run-shape-guard' -- .claude/settings.json returns zero commits: the string has never appeared there. The wired PreToolUse entries are the engine dispatcher, issue-search-guard, issue-read-guard, board-move-guard, connector-verb-guard, connector-allow-guard, fanout-guard. Every other reference to run-shape-guard in the repo is prose — two memories, five workflow comments, a MUTANT_GATES row, a doc comment in exec.rs, and AGENTS.md:144's claim "Gated by run-shape-guard."
So mise-tasks/run-shape-guard is 267 lines with two bats suites and a mutation census row, enforcing nothing. Its foreground-sleep deny text (:213) is precisely the coaching that would have redirected the measured session — "act on its exit; the harness re-invokes you … never poll in the foreground" — and it has never been delivered to anybody.
This is non-negotiable rule 2 failing one level up. The guard's own header says "Prose is feedforward only (non-negotiable rule 2), and the session that hit this had read the prose." The guard then shipped as prose itself: the mechanism landed, the wiring didn't. AGENTS.md:144 asserting a gate that does not exist is the worst half of it — a reader who checks the claim stops looking. CLOUD-400 is the same shape one layer down (batten hook wired into no harness, every mediated_call row gating nothing) and was closed as a duplicate of the cutover; this is that class recurring for a shell guard that the cutover leaves in place.
Note what this does not say: foreground sleep is still refused in practice, by the harness's own block. That block is what redirected the measured session into run_in_background: true — out of a gated shape and into an ungated one — which is Defect B.
Defect B — run_in_background: true is treated as proof the wait is well-formed.
run-shape-guard:197 skips the entire foreground-sleep family whenever the flag is set. The comment at :193 explains why the flag was chosen, and it is right about the case it has in mind. But :213 states what actually makes a wait correct:
background a command that EXITS when the condition holds (
until <test>; do sleep 1; done), which is a background wait and is allowed.
sleep 590; tail -6 /tmp/land.log has no exit condition. It is a wall clock standing in for an event — the exact defect mem:workflow/landing-loop names ("a guessed sleep standing in for 'the background thing has happened'"), and it is what was written 490 times. The flag moved the poll out of the guard's view rather than making it correct. The predicate the comment already describes — is there an exit condition — is decidable from the command string.
Relation to CLOUD-489 (In Progress, no branch, no code on main)
CLOUD-489 narrows the same :197 exemption and its argument is adopted here unchanged. Its predicate is over loop conditions: family 1, a loop whose condition invokes pgrep/kill -0/ps; family 2, a loop whose condition reads a harness task-output path. Both require a loop construct to be present. The measured shape here has no loop at all — a bare sleep, then a tail. CLOUD-489's families do not match it, and its own carve-out language ("a command that exits when the condition holds") presumes the loop this shape omits.
So this is the complement, not a duplicate: CLOUD-489 refuses the wrong thing to wait on, this refuses waiting on nothing. They meet at the same line and want the same deny text. Defect A blocks both — neither predicate enforces anything until the guard is registered — hence blocks CLOUD-489.
Refinement — Ready
- Source of truth (§1).
mise-tasks/run-shape-guard, which already owns every run-shape refusal and already readstool_input.run_in_background;.claude/settings.jsonplushooks-wiring-check'sDECLAREDtable for the registration. No new task, no second guard. - Mechanism as a computable predicate (§2). Two parts.
- Registration. A
PreToolUseentry on matcherBashin.claude/settings.jsonpointing at$CLAUDE_PROJECT_DIR/mise-tasks/run-shape-guard, plus itsmise-tasks/run-shape-guard <this-key>row inhooks-wiring-check'sDECLARED(:105-116). Without the row the entry iswiring-sibling-command; without the entry the row iswiring-declaration-stale. Both directions already gate, so the registration is decided by an existing check rather than by review (CLOUD-713/777). - The exemption narrows from "
run_in_backgroundis set" to "run_in_backgroundis set and the stage resolving tosleepis inside anuntil/whileloop". Judged per stage after the existing heredoc-drop and quote-scrub, over the already-scrubbed element, usingresolve()and theelements[]/seps[]split the file already has. A predicate over the scrubbed element, not a new parser.
- Registration. A
- Blocker to clear in the same change (§8, a precondition rather than a dependency).
hook-pin-checkrefuses a hook registered by path that shells out to a mise-pinned tool, because a by-path invocation does not get mise's env and every read here is fail-open — it would silently allow.run-shape-guard:50-51and:197read the payload withjq. The three routes, and why the third:#PIN-OK: jq— the exemption exists for reads that no extraction surface can serve (mcp-attach-check's settings/log files). A hook payload is exactly whatpayload-fieldserves, so claiming it here is claiming the wrong exemption.- register as
mise run -q run-shape-guard— unblocked (hook-pin-checkdoes not judgemise runregistrations), but pays ~203ms of task-runner startup on everyBashcall, which is the hot path CLOUD-479/435 measured and moved three hooks off. payload-fieldfor all three reads.commandandhook_event_nameare already served.run_in_backgroundis not:hook::Field(hook.rs:1396) is a fixed allowlist with no such variant — which is CLOUD-613's observation that "the last two families need facts the mediated envelope hides: the call'srun_in_background." The enum is#[non_exhaustive]and its doc states "Growing it is a deliberate edit here, which is the point", so addingField::RunInBackgroundis the sanctioned edit, not a workaround. It is a boolean about the call, never content, so rule 4 holds:Field's safety argument is that it can never nameEnvelope::input, and a bool cannot carry a secret.
- Effect (§3).
run-shape-guardis aPreToolUseguard reading a payload on stdin; it adds no command to the surface, so noeffectdeclaration and no change to the derived read-only allowlist.batten payload field run-in-backgroundis a read on an existing read-only verb. - Output & exit contract (§5). Unchanged shape:
permissionDecision: "deny"JSON on stdout, exit 0, fails open on anything unparseable, honoursBATTEN_RUN_SHAPE_BYPASS=1. Pointer-only. The deny text gains the remedy half — the completion notification already wakes you (measured 523/524 this session, and CLOUD-489's three-arm controlled measurement showed even a near-idle backgrounded loop is notified and not evicted), andmise run alive(CLOUD-425) for a single push-based phase read when the question is "is it still going" rather than "has it finished". An agent refused without being told what to do instead writes a different poll. - Commit / bump (§6).
fix(run-shape-guard)for the guard, wiring and doctrine → patch.feat(hook)forField::RunInBackground→ non-breaking (#[non_exhaustive]);mise run semverdecides, not this paragraph. - Test obligation (§7).
tests/run-shape-guard.bats, in the suite's existing idiom (:14-25: synthesize withjq -nc, pipe to the script, assert on$output— never exit status, since the guard always exits 0). Mutation-checked per CLOUD-418: a new#MUTANT slug|sed|case-substringrow beside:44-45, sinceMUTANT_GATES(mise.toml:316) already lists this task and a new family with no row failsno-mutant-declared.THE MEASURED SHAPE:— backgroundedsleep 590; tail -6 /tmp/land.logis denied. Pins this incident, matching the naming convention at:39and:105.- Backgrounded bare
sleep 300— denied; waits for nothing and reports nothing. - The allows carry equal weight (CLOUD-199 — a guard with false positives gets bypassed): backgrounded
until mise run alive | grep -q land; do sleep 5; donestays allowed;until curl -sf …; do sleep 5; donestays allowed; a backgrounded long-running command with nosleepstays allowed; a foreground command with nosleepstays allowed;sleepinside a quoted span or heredoc is untouched. - The denial names the remedy — assert the text contains
mise run aliveand the exit-notification sentence, not merely that it refused. - Wrapper trio (
gh-guard.bats:105-125convention): allow is silent, fails open on unparseable input, honoursBATTEN_RUN_SHAPE_BYPASS=1. mise run hook-pin-checkgreen with the guard registered by path — the blocker, asserted rather than remembered.mise run hooks-wiring-checkgreen — theDECLAREDrow and the entry agree, in both directions.
- Doctrine, the feedforward half (rule 2: prose ships with its mechanism, never instead of it).
AGENTS.md:138-139— "Backgrounding keeps the session alive and re-invokes you on exit; an idle turn gets the VM reclaimed" — is two clauses that the measured session collapsed into "stay busy or die", then satisfied with make-work sleeps. Split them so they cannot be read as one: a backgrounded task's exit notification is the wake-up, and idling until it arrives is the designed state; "an idle turn" means a turn with nothing backgrounded; and perAGENTS.md:147it is committed-and-pushed, not activity, that survives a reclaim. VerifyAGENTS.md:144's gate claim is true once the registration lands, and correct.claude/rules/toolchain.md's guard bullet to the new predicate and bypass.
Acceptance
run-shape-guardis registered and fires: aBashcall carryingsleep 590; tail -6 /tmp/land.logwithrun_in_background: truecomes back refused, with text namingmise run aliveand the exit notification. Behavioural, end to end — not "the bats suite passes".hook-pin-checkandhooks-wiring-checkare both green with it registered by path.- Every genuinely-well-formed wait stays allowed, pinned by test in both directions. If this deny fires on a correct wait it will be bypassed and is then worse than nothing — which is why the allow rows are not padding.
plan-hold(CLOUD-491/515) was a gate in exactly this area built on an unmeasured premise; it was measured twice, failed twice, and was removed. The premise here is measured: 490 polls, two of which changed a decision. AGENTS.md:144's claim is true rather than aspirational, and:138's sentence no longer licenses the misread.- Anti-vacuity per CLOUD-418: removing the rule turns the new cases red under
mise run mutant.
Landed — verified against main (170c7c4) on 2026-08-22. The "has never been registered" premise is now historical.
Recording this so the title and Defect A are not read as current state. Both defects are on main via #605:
Defect A — closed. .claude/settings.json carries a PreToolUse entry on matcher Bash invoking $CLAUDE_PROJECT_DIR/mise-tasks/run-shape-guard.sh, and mise-tasks/hooks-wiring-check.sh's DECLARED table carries the matching row mise-tasks/run-shape-guard.sh CLOUD-821 (:178). Both directions gate, so the registration is decided by a check rather than by review — exactly what §2's part 1 specified. AGENTS.md's gate claim now resolves: the repository doctrine cites run-shape-guard for a shape the guard actually refuses.
Defect B — closed. The exemption is narrowed as specified: run-shape-guard.sh:324 requires an until/while construct in the scrubbed command before run_in_background exempts a sleep, and the header states the predicate as "a backgrounded sleep-then-read is a timer, not a wait" (:97). It ships with its mutation row — #MUTANT background-timer-exempt flips waits_on_condition and turns the measured-shape case red — so the anti-vacuity clause in Acceptance is satisfied by a mechanism rather than by assertion. The deny text names both remedies: the exit notification (measured 523 of 524) and mise run alive.
The guard is 647 lines now, from 267 — it has grown the commit-stdin and cargo-substitution families since this row was filed, each with its own mutation row.
Observed live this session, which is the behavioural acceptance clause rather than "the bats suite passes": the sibling verdict-not-discarded row refused a mise run piped into tail, and no-tool-substitution refused four repo-path reads. The registered guards decide.
What remains, and it is not this row
mise-tasks/run-shape-guard.sh is row 11 of CLOUD-312's thirteen-row retirement inventory. Its destination there is config, partially reachable today — Field::RunInBackground landed, and its doc records that CLOUD-613's "the last two families need facts the mediated envelope hides" is half answered — and the heredoc-binding family still needs a fact the envelope does not carry, which is CLOUD-613's remaining half.
So the split is: this row is done at its own scope (register it, and narrow the exemption); the script's retirement into the committed authority is CLOUD-312 row 11, ordered behind CLOUD-613. Keeping those in one row is what would make this one unclosable.
CLOUD-1210 118 integration test targets link 14.1 GB of near-identical binaries, and the retirement campaign adds one per retired gate
Why
crates/batten/tests/*.rs is 118 tracked files, and crates/batten/Cargo.toml declares no [[test]] entries — so cargo autodiscovers one test target per file and links 118 separate binaries, each against the whole closure (gix, regorus, syn, clap, jsonschema, hyper/rustls).
Measured, this container, 2026-08-30:
target/debug/depsholds 122 extension-less artifacts totalling 14.1 GB, averaging ~116 MB each; the directory is 17 GB.- Partial rebuild (119 crates against a warm remainder): **309s wall, 142.0s nextest **
Summary. A true cold build was never measured and this number must not be quoted as one. - Warm, nothing changed: 231s wall, 127.0s
Summary, 5.9s freshness check — leaving 97.7s, 42% of the loop, unattributed. That residue is NOT this row's to claim: see the withdrawal below.
An earlier revision of this row quoted a 1376s cold arm and "~90% of it compile and link". Both were derived by guessing when the run started and ended rather than reading it, and both are withdrawn — which is CLOUD-1208's point arriving on its own sibling.
crates/batten/src/prune.rs:262-269 already records this artifact set — 114 binaries, 13411.9 MB, 86.8% of deps, measured 2026-08-20 — but reads it as a disk problem (CLOUD-766). It is the same fact, and nobody has priced it as latency. CLOUD-1158 is a third reading of the same count, as a moving basis under [prune]'s floors.
Prior art, and the mechanism here is adopted rather than invented. matklad, Delete Cargo Integration Tests, is the canonical statement of this defect: "rustc needs to repeatedly re-link the library crate with each of the integration tests", producing "a significant compilation time blow up for tests". Its recommended layout for a large codebase is exactly tests/it/main.rs plus one module per former file — the §3 below. Cargo's own repository did this refactor and measured it: the time to compile the test suite fell 3x and the size of on-disk artifacts fell 5x. A 5x artifact reduction against this tree's 14.1 GB is the same order as the byte problem CLOUD-766 is open on, arriving from the cause rather than the symptom.
A SECOND SAVING THIS ROW CLAIMED AND HAS NOW WITHDRAWN. An earlier revision said the 97.7s warm residue was nextest's per-binary list phase and therefore fell with the target count — making it this row's second measured win. Falsified 2026-08-30. A zero-match filter run (-E 'test(zzz_no_such_test_zzz_)') pays the freshness check AND the full enumeration and then runs nothing: Starting 0 tests across 119 binaries (3167 tests skipped), total wall 1.75s. The list phase is 56x smaller than the residue it was supposed to explain, so consolidating targets does not recover it and this row must not be justified by it. What the residue actually is remains unknown — CLOUD-1208's subject, arriving on its sibling for the second time.
What survives is the prior art, and it is enough on its own: 3x off test compile time and 5x off on-disk artifacts, measured by Cargo's own repository. This row stands on that and on the 14.1 GB census, not on a residue nobody can attribute.
A third saving the prior art names, stated as a possibility rather than a claim. Splitting tests across binaries means a shared expensive fixture is built once per binary rather than once; consolidated, a OnceLock/LazyLock setup is paid once. This tree materialises scratch git repositories per fixture through real git subprocesses (tests/common/mod.rs::git_in), so the cost is live — but whether any of it is actually shareable is a question for the migration, and this row does not price it.
Root cause, and why it gets worse on its own. The convention is one integration target per contract surface, which is locally correct and has no stopping condition — the same shape CLOUD-365 records for case counts. CLOUD-843's retirement campaign adds a target per retired gate, with ~147 shell suites still standing, so the count is monotone in the campaign's own progress. prune.rs's comment records the count moving 110 → 114 → 118 across three readings in ten days.
TWO RETIREMENTS ARE IN FLIGHT AGAINST THIS FILE DOMAIN, so the interaction is live rather than anticipated: CLOUD-1145 (derived-check) and CLOUD-1161 (ci-local-parity), both In Progress on 2026-08-30, and each mandated to add a top-level crates/batten/tests/*.rs. Whoever lands this row rebases across them and re-homes their files into a group as a mod — a mechanical move, not a content conflict, and not something to engineer around.
A THIRD READING, measured 2026-08-31: on this container the loop does not close
The existing readings price this as latency (compile time) and as bytes (deps at
14.1 GB). Both are true and both understate it. Measured across four consecutive
mise run land attempts in one session:
- **One **
verifylap consumes 25403 MB, reported bytarget-prune's own
lap-close line: "the lap opened on43fc2414consumed 25403MB". - The container's writable allowance leaves ~24 GB free after a full purge of
target/debug/deps+target/tmp. - So a lap consumes more than the machine can hold, and
target-prunecloses it
below its own warm floor every time:free 5680MB, floor 7264MB.
The consequence is not slowness, it is that verify can run at most once per**
**manual purge and then wedges. Three of the four land attempts in that session
died at target-prune before reaching CI, and each recovery was a hand-rm of
target/debug/deps — which CLOUD-1218 records as inverting the ratchet, so the
remedy is itself a defect.
target-prune's refusal also names the wrong remedy, which is what sent the
first recovery in the wrong direction: it says "Free space outside ./target, or*
*start a fresh session" while 17 of the 20 GB were **inside **./target, in
deps, in artifacts its warm-basis logic declines to reclaim. Following the
advice as written leads to deleting cross-compile roots — measured earlier the
same session at 2.3 GB freed and 8.3 GB spent rebuilding them on the next lap.
This is the same 14.1 GB artifact set the row already counts; what is new is that
it is now larger than the headroom, so the failure mode changed from "slow" to
"cannot complete". Raising to High on that basis: a contributor on this container
cannot run the repository's own required gate twice without hand-purging, which is
the condition CLOUD-1218 exists to prevent.
REOPENED 2026-09-01 — this row reached Done without being built
It was closed on 2026-08-31 and none of §3 exists. The evidence is in the row's own metadata and in the tree:
startedAt: null. The row went Backlog → Todo → Backlog → Todo → Done, never In Progress.- No PR attachment. Its sibling CLOUD-1211 carries PR perf: the gate tail 84.4s→37.2s and the Rust suite 100.2s→48.6s, each measured against a null #775; this row carries nothing.
- Checked against
origin/main(a673667), not a stale checkout: 144 top-levelcrates/batten/tests/*.rs, zero[[test]]entries incrates/batten/Cargo.toml, and nocrates/batten/tests/*/main.rs. The grouping §3 specifies was never done. - **The ratchet was never written. **
batten.tomlcarriestests-not-deletedandbats-tests-not-deleted, both token counts of#[test]occurrences; nopolicy/*.regomodule refuses an added top-levelcrates/batten/tests/*.rs.
It was a sweep, not a decision. Six rows carry a completedAt inside a 1.7-second window on 2026-08-31: CLOUD-699 (14:19:59.748), CLOUD-1211 (14:19:59.865), CLOUD-1210 (14:20:00.294), CLOUD-592 (14:20:00.588), CLOUD-1208 (14:20:00.756), CLOUD-1216 (14:20:01.463). No human sets six statuses in 1.7s.
The count grew while this was being re-groomed
In the 8 commits between one container's clone (1a79d8a) and origin/main (a673667), top-level test targets went 142 → 144: commit_admission.rs and mediated_admission.rs added, none deleted. That is this row's own "the campaign adds one per retired gate" observed live, over hours rather than the ten days prune.rs recorded — and it is a stronger argument for the ratchet than the 110 → 114 → 118 series the row was filed on.
§8 is discharged, and the measurement it was blocked for now exists
CLOUD-1208 is Done, so the blocker is cleared. Measured 2026-09-01, this container, 4 cores, cargo nextest run --workspace:
| phase | cold | after touching one src/*.rs |
no-op |
|---|---|---|---|
| build | 277s | 48.0s | 0.4s |
| run | 139s | 92.6s | 92.6s |
One test target rebuilds and links in ~1.0s (touched tests/walker.rs alone: 1.03s cargo). So the 48s decomposes as ~10s lib compile plus 144 targets × ~1.0s at 4-wide ≈ 36s of linking — the link step is up to ~75% of every rebuild, and a rebuild is what a developer pays per edit.
Read the 75% as an upper bound, not a point estimate. It was measured with touch, which changes mtime with identical content: cargo fingerprints on mtime so all 144 targets relink either way (the ~36s stands), but incremental codegen has nothing new to do, so the lib-compile half is understated and a real edit shifts the ratio toward compile. The paired before/after this row's acceptance requires must use a real edit, not a touch.
What this measurement does NOT support, stated because the row has withdrawn two over-claims already: it says nothing about the 92.6s run phase, which is separately measured at 4.01x parallel efficiency on 4 cores and has no scheduling headroom left. Consolidation is a build-time win only, exactly as §2 already says.
Refinement — Ready
-
**Source of truth (§1). **
git ls-tree -r --name-only <ref> -- crates/batten/tests/, filtered on^crates/batten/tests/[^/]+\.rs$, against the artifact census intarget/debug/deps. NOTgit ls-files 'crates/batten/tests/*.rs', which this clause specified until 2026-09-01: git's default pathspec*matches/, so that form also matchescrates/batten/tests/common/mod.rs— a filecommon/mod.rs:19-21states is deliberately not a target — and reports one target too many.prune.rs:262-269is the prior reading and the one this row must not contradict silently. -
Mechanism (§3). Group the files under
crates/batten/tests/<group>/with amain.rsdeclaring onemodper former file. Cargo compiles atests/subdirectory as a single target exactly when it holds amain.rs— a facttests/common/mod.rs:19-21already states and relies on, which is whycommon/is not itself a target today. Grouping is mechanical and case-preserving. -
The ratchet that stops it re-growing (§3), and it counts TARGETS rather than FILES — the distinction is load-bearing. A
[[ratchet]]row on the count of cargo test targets, direction non-increasing.ratchetis an existing rule kind (CLOUD-55) andtests-not-deletedis the precedent. Without a ratchet the campaign restores the count one retirement at a time and this row buys a one-off saving instead of a property.A FILE-count ratchet would refuse CLOUD-843**'s entire campaign, which is why the spelling is not free. **
.claude/rules/toolchain.md's two-shapes rule requires a retirement to land its predicate as apolicy/*.regomodule **plus a **crates/batten/tests/*.rstier, and.claude/rules/policy-modules.mdstates that tier "iscrates/batten/tests/*.rsand never a.bats". So every retirement adds a top-level test file by mandate — which is exactly whyprune.rsrecorded the count moving 110 → 114 → 118 across three readings in ten days. A ratchet over top-level FILES would fire on the next correctly-executed retirement, and the campaign would have to switch it off: the shape a gate does not survive.Counting targets removes the collision by construction — a retirement's tier lands as a
modinside an existing group, the target count does not move, and the ratchet never sees it. That is matklad's recommended layout anyway, so the campaign adopts nothing it was not already heading for. But the convention has to be written down when this lands, or the next retirement author creates a new top-level file and trips a gate nobody warned them about. -
The assertion that makes this safe (§3) — upstream-documented AND asserted here. nextest's design doc states it plainly: "A key factor distinguishing nextest from
cargo testis that nextest runs each test in a separate process", giving memory isolation, state isolation and independent termination (why-process-per-test). So consolidation changes the link count and nothing a case can observe. The claim is load-bearing — if it were false this row would be trading isolation for speed — so it ships as a case pinning that a test's visible environment does not depend on its target, rather than resting on the citation alone. -
What consolidation does NOT buy, stated so it is not over-claimed (§2). It does not reduce the number of test PROCESSES: nextest already spawns one per case, and 3167 of those remain. It does not recover the 97.7s residue — measured, the list phase is 1.75s. The saving is in link count and artifact bytes, and in compile time by the prior art's measurement. Nothing else.
-
Deliberately not in scope (§2). Deleting or merging any test case;
tests-not-deletedmust stay green across the whole change. Also out of scope: the[prune]floors, which read the same count and are CLOUD-1158's — but that row's basis moves when this lands, so it is named here rather than left to discover it. -
Output (§7). Counts and target names.
Test obligation
tests-not-deleted green across the change; the target-count ratchet fails on a re-added top-level file; and the isolation case above fails if a grouped test can see something a standalone one cannot.
Commit / bump (§6): test(harness) — no bump. Not breaking for the consumer surface or the library surface: no shipped verb, no public item and no config key moves; the change is entirely under tests/.
Blockers (§8): blocked by CLOUD-1208, because the saving must be measured before and after rather than claimed — the prior art's 3x/5x is a measurement of Cargo's tree and not of this one, and the residue this row once claimed has been falsified. relatedTo CLOUD-766 (same artifacts, disk reading), CLOUD-1158 (same count, as a prune basis), CLOUD-843 (the campaign that grows it), CLOUD-365, CLOUD-352.
The mechanism, RESOLVED (2026-08-30) — this row's last unrefined clause.
What was undecided: §3 names kind = "ratchet", and that kind cannot say this. Its fields are glob, pattern, direction, base, severity, and its own doc defines it as "the total occurrences of pattern across files matching glob" — a token count inside files. Every committed row is that shape: tests-not-deleted counts #[test], bash-surface-not-growing counts #MISE description=. A cargo test-target count is a property of the directory structure plus Cargo's autodiscovery, and no glob+pattern pair computes it.
The answer is a tree-scoped policy module, and the fact it needs already exists. input.tree["base-delta"] is declared in schema/policy-input.schema.json with added, edited, deleted, base-lines, base-date and code-changed; policy/filed-here.rego already reads it. So the predicate is:
refuse when
base-delta.addedcontains a path matchingcrates/batten/tests/*.rsat the top level — one segment pasttests/.
That is the non-increasing target count stated structurally, and it resolves each problem the open question named. No token count — it asks about paths, which is what a target count is a property of. No base-rev position to take — the base delta is the comparison, so the hard half of the ratchet spelling disappears rather than being solved. No spawn — a command row was the other candidate and CLOUD-843's campaign is retiring exactly that shape.
Why the file spelling is sound here when a [[ratchet]] file count was not. Cargo autodiscovery makes top-level tests/*.rs and test targets 1:1, and a file inside a group directory (tests/<group>/foo.rs) is not a target — so "no new top-level crates/batten/tests/*.rs" is "the target count does not grow", exactly. The earlier objection was to a [[ratchet]] counting files, which fires on any added file including a group mod; this fires only on an added TOP-LEVEL one, which is the thing that mints a target. A retirement landing its tier as a mod inside a group is invisible to it, which is the property that keeps CLOUD-843's campaign running.
One trap for whoever writes it. .claude/rules/policy-modules.md's list of input.tree.* keys does not name base-delta (or symbols), though the generated schema declares both and filed-here.rego reads the first. That file states rules-drift holds its lists to the schemas, so the omission is either a drift the gate is not catching or a list that needs updating — worth resolving in the same change, because an author who checks the documented list will conclude the fact does not exist and reach for a worse mechanism. That is exactly the silent-dead-gate class that file is otherwise about.
Acceptance
- The target count falls and the ratchet holds it.
- The delta is quoted from CLOUD-1208's harness against its measured null, not from a hand timing.
- No case is deleted, and the isolation assertion exists and is shown able to fail.
|
Warning Review limit reachedNext included review available in 2 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 (14)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughAdded vendored-prompt review dispatch with digest-keyed caching, readiness probes, strict pointer parsing, and persisted provenance. Added review facts, schemas, rule wiring, and tracker-body pressure-test enforcement with an independent cutover. Added the Merge Risk: ⚪ Minimal · up to This PR adds review-dispatch facts and related policy, schema, and hook updates; no actionable merge-blocking risk remains beyond normal checks and review. Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
17bcaeb to
6194f87
Compare
The premise the revert rests on is false, and it is measurable todayThis PR withdraws That zero is producible, and the mechanism has been in the tree the whole time. The measurement this branch could not takeMeasured on this container, 2026-09-02, and the reason it reads differently here is the reason the whole class was invisible:
The installed binary predates CLOUD-1314, so
|
980cdee to
8f83070
Compare
6c76c0f to
0e2f700
Compare
…s itself
`hooks-wiring-check` was red in every session in this environment, which is the
pre-commit gate refusing every commit on a condition no commit in this repository
can reach. Two independent causes were found. ONE OF THEM IS NO LONGER THIS
COMMIT'S, and saying so is the point of this paragraph rather than a footnote.
WHAT THIS COMMIT NOW CARRIES is the second cause only: CLOUD-1314's deletion of
both merged `declared` rows, on the premise that "both registrations are gone and
both programs are deleted". They are not gone. Measured 2026-09-02:
`~/.claude/stop-hook-git-check.sh` and `session-start-git-identity.sh` both
present, mtime 04:22 — three hours AFTER the commit that deleted their rows at
01:18 — and `launcher-settings.json` registering both. The rows are back, naming
CLOUD-1079, which is the row `batten.toml` already identifies as owning the
environment half.
WHAT IT NO LONGER CARRIES is the withdrawal of `[hook] exclusive = true`. This
commit removed it, on a measurement: the note read "`siblings: 0` and
`merged_siblings: 0`", and measured on the declaring commit it was
`merged_siblings: 2`. `origin/main` reached the same withdrawal independently,
under CLOUD-1324, with a fuller note — so the rebase left this half with nothing
to apply and the `batten.toml` hunk is gone. The `Admits:` block that rode with it
is gone too, which is the gate working rather than a tidy-up: an admission
naming a write this commit does not make is exactly the declared-but-unreachable
class the branch spent the session closing.
AND MAIN'S NOTE NEEDS THIS COMMIT TO BE TRUE. It says "The two rows are therefore
back in `policy/harness-declared.json` naming CLOUD-1314"; on `origin/main` that
file is `{}`. The restoration below is what makes the landed sentence describe the
landed tree.
ITS ACCEPTANCE CASE IS WITHDRAWN RATHER THAN DELETED, because a case removed is
indistinguishable from a case that never held. `test_the_retired_launcher_hooks_
are_refused_if_they_return` asserted a property the world does not have;
`test_the_launcher_hooks_are_declared_rather_than_refused` asserts the one it does,
and `test_an_undeclared_hook_beside_the_declared_pair_is_still_refused` is the
direction that stops the withdrawal being satisfied by a module which simply
stopped refusing on merged surfaces. `spent` and `stale` still watch both rows, so
they leave when the launcher stops writing them and not before.
WHAT IS NOT DONE HERE, and why. A per-surface `exclusive` — declarable on the
committed side while the `$HOME` pair stays a count — is what
`policy/harness-wiring.rego`'s own note recommends and is strictly better than
waiting for a global zero. It narrows a landed refusal, so it owes a groomed
`Weakens:` clause and a row of its own rather than riding in on a branch about
something else. CLOUD-1160's shape; CLOUD-1079's environment half.
`policy test`: 45 bundles, 571 passed, 0 failed. `hooks-wiring-check`: 5
harnesses, 0 unwired, 0 findings.
Refs: CLOUD-1314, CLOUD-1079
…n read CLOUD-472. `Fact::Review` is the second occupant of `Cost::Effect` and the third adopter of the delegated-analyser shape: `secrets.rs` (ripsecrets), then `symbols.rs` (clippy), now this. What carries across is a declared program, flags pinned beside the parser, provenance recorded because a fact whose meaning depends on an unrecorded tool version is not canonical, and one invariant verbatim — clean is never inferred from a stream that failed to parse. WHAT IT ANSWERS IS DELIBERATELY NARROW: that a particular prompt ran over these exact bytes. Not whether the review was good and not whether its findings are real, because those are judgements and non-negotiable rule 3 forbids a gate deciding one. A gate over this fact refuses ABSENCE, which is a comparison of two digests, and the agent's findings reach a module as pointers that carry no claim to weigh. That bound is what makes an LLM in a resolution path legal, and the schema is shaped so it holds structurally: there is no string field an agent's prose could occupy. WHY THE CHEAPER TIERS DO NOT SUBSTITUTE. `ready-lint` gates the SHAPE of a refinement block, and shape is what an author optimises against once the gate exists — the measured failure that opened this row, where every clause was present and none had been pressure-tested. `obligations-bound` binds a §7 entry to a killer mutation but only at implementation time; at refinement there is no code, no case file and no `#MUTANT` row for it to reach. Confirming a named prompt ran over these bytes is a hash comparison no better-shaped prose can satisfy, because the prose is the input to the hash. SPAWN ON MISS, READ ON HIT — `step-receipt`'s pattern rather than a new one. A review costs minutes and tokens and `check` runs every landing lap, so an unconditional resolve would be unaffordable and the gate would be switched off. The record is keyed by (prompt digest, subject digest), so the agent runs once per unique subject and every later lap is a cache hit. The keying is the anti-staleness property rather than an optimisation: edit the ticket body or push a commit and the record lives under a name nothing looks up. THE DISPATCH IS THE ENGINE'S, and that is the whole difference from a producer-writes-outside store. `tool-verdict` has the identical read shape and is measured dead — `validator-verdict-clean` reads a record nothing ever writes, so the predicate resolves `null` on every real checkout (CLOUD-1265) — because somebody has to remember to run the tool and pipe its output. A fact the engine resolves has no such gap, and needs no spelling per harness. THE PROMPT IS VENDORED, compiled in the way `policy/presets/**` are, so its digest is a constant of the build. That is what makes "a particular prompt" a checkable claim: a consumer cannot satisfy the gate by pointing it at an easier one, because the digest in the key is not theirs to choose. Every failure path leaves NO record — runner missing, non-zero exit, unparseable stream — so a broken agent is indistinguishable from one that never ran and both refuse. `pointers_in` rejects the whole stream rather than filtering it, because a parser that skipped what it did not understand would store whichever lines looked like pointers and call the rest absent. `ReviewQuery` lives on the rule row rather than in a top-level table, for `ToolQuery`'s reason: the declaration and the rule that reads it are one object, so a row cannot name a review nothing enables. Not yet landed, and named here so the gap is visible rather than implied: the consumer-side rego module that refuses absence, the two test tiers, and a `[[rule]]` row that enables any of it. This commit lands the fact and its resolution; nothing reads them yet. Refs: CLOUD-760, CLOUD-1171, CLOUD-1265, CLOUD-418 BREAKING CHANGE: `Fact` gains a `Review` variant, `Resolved` gains a `review` field, and `ReviewQuery` is a new constructible struct. `semver check` names the three lints — `enum_variant_added`, `enum_no_repr_variant_discriminant_changed` and `constructible_struct_adds_field` — and it is right about all three: a downstream exhaustive `match` on `Fact` stops compiling, and so does a struct literal for `Resolved`. Declared here rather than on a later commit because this is where the variant and the field arrive; the `probe` and `prompt_arg` fields a later commit adds to `ReviewQuery` are the same break widening, not a second one. Below 0.1.0 release-plz bumps the patch whatever the type says, so the `!` buys a reader the notice rather than a different version. Admits: e2396335346ddac886cb3b7db31a58250f0592684ec8222f28792c5e9032d4ba Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: .serena/memories/core.md Admits-head: 9f35e41 Admits-epoch: a1a148a28e969c732dd6bf887133eecb19230d317fefb66fb2b847035606d3b9 Admits-author: alec@wenzowski.com Admits-prev: 4ec572594ee30d8023fbc708c2772377d39a85bea5105083bef41de7b83ec75b Admits-answer-lost: `module-map-check` refuses the commit outright: a new module with no map row is a gate failure, not a style note. The alternative is landing `review.rs` with the map silently stale, which is the drift the map exists to prevent. Admits-answer-precondition: `.serena/memories/core.md` carries the per-module map, and `module-map-check` fails a `crates/batten/src/*.rs` with no row. This commit adds `review.rs`, so the row is obligatory and there is no surface that writes it — the map is prose in a memory, and no verb appends to it. It lands in the diff a reviewer reads, beside the module it describes. Admits-answer-rejected-route: `config read first` does not apply — the map is a memory, not config, and reading it is how the missing row was identified. `patch run first` has nothing to patch: no verb writes a module-map row, which is why `module-map-check` is a gate over prose rather than a generator.
CLOUD-472. `review::dispatch` wrote the vendored prompt to the child's stdin and
nothing else, so the agent was told what to look for and never told what to look
at. Its answer was a review of nothing, while the record was keyed to bytes it
had never seen — a record that reads as a completed review and is not one, which
is strictly worse than no record, because the gate reading it would report clean.
THE SUBJECT TRAVELS AS A POINTER, NEVER AS BYTES, and that is Batten's law rather
than an economy. `judge.rs` states it at its own head — "sensitive or bulky
content is reduced to a pointer and never dumped into a model's context" — and
names the LLM judge "the ONE component that inverts it". A review dispatch must
not be a second inversion. So what crosses is the prompt, the subject's path and
the subject's digest, and the agent reads the bytes with its own tools under
whatever access its operator gave it.
It was worth considering the other route and it is worth writing down why it
loses, because the obvious fix is to hand the bytes to `judge::assemble` and
inherit its boundary:
* `judge`'s fail-closed rule reads a span with NO PATH PROVENANCE as protected
and refuses the whole invocation. That is right for a span and fatal here: a
tracker row's body legitimately carries an issue key rather than a repository
path, so the grooming subject — the one this row exists for — would have been
unimplementable.
* `[judge] raw` is empty by this repository's deliberate posture, so `assemble`
would have withheld the bytes and sent a pointer and a hash ANYWAY, after
considerably more machinery.
* A second component inverting the pointer-only law is a widening no consumer
asked for, and the widening would have been invisible: nothing in the config
would have said that enabling a review row also started shipping file
contents outward.
The claim the record makes is unchanged. The digest is still taken over the bytes
on disk, so a record still cannot be keyed to anything but the exact subject, and
editing the subject still leaves it under a name nothing looks up. What changes is
that Batten never becomes the thing that moved somebody's file into a model.
The vendored prompt is updated to match: it states that the subject arrives as
`<subject> <digest>` and must be read by the agent, that a run reporting nothing
is a valid answer — the caller distinguishes that from a run that never happened,
so there is never a reason to invent a finding — and that any line which is not
three whitespace-separated fields discards the whole run.
Refs: CLOUD-135, CLOUD-1265
…r is not guilt
CLOUD-472. `Fact::Review` landed as a store nothing read — the same dead gate its
own commit message spent four paragraphs diagnosing on `tool-verdict`. This is
the reader: `policy/review-dispatched.rego`, its verdict, the row that enables
it, and the compiled tier that proves the engine builds what the module reads.
WHAT THE GATE ASSERTS IS STRONGER THAN "SOMEBODY REVIEWED". Because `resolve`
DISPATCHES on a miss, a review is compelled rather than merely checked, and the
reachable states collapse to three: the agent ran and recorded (clean), the agent
ran and gave nothing usable (refused — the branch's problem), or there is no
agent here to ask (could-not-look). Forgetting is not among them.
AN ABSENT RUNNER IS COULD-NOT-LOOK, AND GETTING THIS WRONG IS WHAT THE FIRST
ATTEMPT DID. The dispatch returned silently on a failed spawn, which made a
machine with no reviewer installed indistinguishable from a branch that skipped
its review — a verdict about the OPERATOR wearing a verdict about the branch.
Measured: it took four unrelated `cli.rs` cases red, because a fixture has no
agent and never will. `symbols::resolve` keeps the identical pair apart in the
same words — "the analyser is not installed, or could not be spawned" is
could-not-look — and `Dispatch` now names all three outcomes so the distinction
is in the type rather than in a comment.
GATED ON THE ROW'S OWN DELTA. A review is owed when the thing to review CHANGED,
so the row declares `delta_sources` over its subject and the module refuses only
against a non-empty delta. Ungated, the refusal fires on every checkout that has
never dispatched — every fixture and every fresh clone — which is the shape that
gets a gate switched off rather than satisfied. `plan-complete` carries the
identical narrowing for the identical reason, measured the same way.
IT REFUSES ABSENCE AND NOTHING ELSE. What the agent CONCLUDED is never read:
that would be a model verdict wearing an exit code, which non-negotiable rule 3
forbids, and the fact is shaped so it cannot be written — `findings` are
`{path, line, clause}` pointers with no field prose could occupy. A review that
pointed at something is as clean as one that pointed at nothing, deliberately:
pricing findings is how you buy an agent that reports none.
THE OPPOSITE ARM FROM `forge check red`, and the asymmetry is the design. That
module refuses only a PRESENT-and-red verdict and reads absence as
could-not-look, because the forge is a third party that may legitimately not have
judged yet. A review this branch was supposed to dispatch is the branch's own
conduct.
THE DISPATCH GOES THROUGH `exec::piped`, THE PLACED CHILD-PROCESS ADAPTER, and
the first attempt did the opposite. It spawned directly and then added `review`
to `policy/spawn-adapters.rego`'s table — which is precisely the move that rule's
own remedy tells you not to make: `spawn place missing` says "route it through
`exec`". `exec::piped` exists for exactly this and its doc comment had already
named the failure: two earlier callers each grew their own `Command::new`, "which
is two spawns in two unplaced modules and one shape written twice". This was
nearly the third. Routing there means there is no new spawn site to place at all,
so the adapter table is untouched, and it buys the shebang resolution ladder a
`#!/usr/bin/env` runner needs on a host where `CreateProcess` refuses an
extensionless program.
The verdict is `prompt run never` under a new `prompt` vocabulary word rather
than a third `review …` class: `review answer missing` and `review read absent`
are about the FORGE's review of a pull request, and a name a reader has to look
up is the one thing the three-word grammar exists to prevent.
Verification: `policy test` 46 bundles / 558 passed; `config lint` 0 smells;
`test:cargo` green.
Refs: CLOUD-760, CLOUD-1265, CLOUD-418, CLOUD-135
Admits: fbe348673db0a5c9304b0d27bd849b6b89d6b3468d23f1f81f6a738c269bcd6c
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: 6019ff2
Admits-epoch: 4d644b4da6de2e07a9c4d19af1bbf102c467b4ab4e83e3a09a0885497a9cfb66
Admits-author: alec@wenzowski.com
Admits-prev: 5556da73126df3569d0b9d711b2111213a3afc3e7b05339a9b7b7ff9bb287557
Admits-answer-lost: CLOUD-472 entirely. `policy/review-dispatched.rego` decides nothing unless a row registers it and declares the `[[rule.review]]` facts it reads, and a module raising a `[[verdict]]` token no row declares fails to load. The alternative is landing the module and both test tiers over a gate the engine never runs.
Admits-answer-precondition: The change adds the `review-dispatched` `[[rule]]` and its two `[[rule.review]]` rows, plus the `[[verdict]]` registry rows the module raises. A rule and its declared facts are config by construction — there is no verb that writes a `[[rule]]`, since a gate that could add its own rows would be a second authority over the committed one. It lands in the diff a reviewer reads, beside the module it registers.
Admits-answer-rejected-route: `config read first` is what this change is FOR — reading the committed config is how the row's shape, its `base`/`delta_sources` narrowing and the vocabulary rows were derived. `patch run first` has nothing to patch: no verb adds a rule, a review row, or a verdict-registry row.
CLOUD-472's originating ask, and the half the landing gate could not reach. A subagent verifies and pressure-tests a row BEFORE it enters the ready queue, because a deterministic confirmation that a particular prompt has run beats any amount of improved tooling — and none of the tooling could serve it. `obligations-bound` binds a §7 entry to a killer mutation but only at implementation time; at refinement there is no code, no case file and no `#MUTANT` row to bind to. Nothing gated this. WHAT IT ASKS THAT NO OTHER CLAUSE CAN. Every sibling in `ready::lint` reads the block and infers quality from its SHAPE, which is precisely what an author optimises against once the gate exists — the measured failure that opened this row, where every clause was present and none had been pressure-tested. This asks whether a PARTICULAR vendored prompt ran over these exact bytes. It is a comparison of two digests, so no model verdict reaches the exit code (rule 3), and better prose cannot satisfy it because the prose is the input to the hash. THE BODY IS THE TRACKER'S, which is the whole of what makes it worth anything. `payload.description` is what the tracker RETURNED — the same source the recorder's `verdict` authority column reads. A digest over a caller-assembled payload would certify whatever the caller typed, measured green three times against text in a local file, once under an id no row carried. COULD-NOT-LOOK PASSES THREE WAYS, and they are three different absences rather than one: no declared cutover (the consumer has not asked), a payload with no creation instant (unplaceable against one), and no runner installed (an environment that cannot be asked). Only a review that was ASKED FOR and did not answer refuses. `graph-check.sh` IS NOT EDITED, and could not be: it is governed by `shell-retirement`, whose only landable shapes are retire-whole or leave-alone. It already enforces `Todo => ready-lint exits 0`, so a finding here gates the ready queue for free. A RATCHET RATHER THAN A FLIP. `pressure_test_required_from` is a sibling of `prose_dialect_required_from` rather than a reuse of it, because the two convert different things and a consumer must be able to move one without the other — folding them would mean the day you demand a structured claims object is also the day you demand an agent dispatch. Without a cutover the flip refuses the standing Todo queue at once and takes the ready frontier dark, which is CLOUD-858's measured shape. `trust.rs` now compares BOTH cutovers from one table rather than two hand-written blocks: they are the same class of weakening — an instant moved later, or dropped — and the second block would be the first copied, which is what goes stale when a third lands. Extracting it was not optional either; the second cutover took `entry_weakenings` past its declared line budget, which is that budget working. `ReviewQuery.path` becomes OPTIONAL, because a refinement block has an issue key rather than a repository path. Demanding one would force a row to name a file it never reads. The tree surface skips a `document` review with no path rather than guessing, and `lib.rs` filters the body kind out of the grammar — so the two gates share one declaration and neither silently answers for the other's subject. THE POSITIVE CASE IS WHAT GIVES THE OTHERS MEANING. Four of the six assert an ABSENCE, and a suite of those passes trivially if the finding can never fire — the shape a dead gate and a clean tree share. `a_runner_that_answers_nothing_ usable_is_refused` drives a stub that exits non-zero and asserts the class appears at exit 2. It was nearly missed: the filter used to run these matched only four of the six by name, and 6/6 green was about to be read as coverage. Verification: `test:cargo` 3986/3986; `config lint` 0 smells; `mutate census` 115 gates, every one enforced or exempt. Refs: CLOUD-858, CLOUD-418, CLOUD-1265 Admits: 842945f1bb989e350d9e822beca3824c822285e88582b6601acfaee08d943334 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: 8335481 Admits-epoch: 200899838d8b40ad40d533695eb90d00972cc6b93e62b7fbfaa498a73f4094ce Admits-author: alec@wenzowski.com Admits-prev: e03ad425042415b5ee8a05a3859b9fa0f13de28eae0df7c82bbf0620653b8ecf Admits-answer-lost: The grooming gate becomes unreachable. `ready::lint` filters the declared rows for `subject = "tracker-body"`, so with no such row the loop is empty and the arm decides nothing on every payload — a gate that compiles, tests green in its own fixtures, and refuses nothing in the real repository. That is the exact dead-gate shape this branch exists to remove, and CLOUD-1265 is it already happening on `tool-verdict`. Admits-answer-precondition: A `[[rule.review]]` sub-table is the one surface that can declare which review a repository owes, and `batten.toml` is the committed authority that holds it — there is no other place to say it, so writing the protected path directly is the only route. The write is additive, one row, and lands in this diff beside the `ready lint` arm that reads it and the six cases that drive it. Admits-answer-rejected-route: `config read first` names this case and does not apply: reading the config is how the sibling `document` row's shape was taken, and reading it again does not remove the need to WRITE a second row. `patch run first` rejected too — nothing derives or generates a review declaration, so a patch would be the same bytes through another door.
CLOUD-472. Reading a reviewer customers actually run refuted three things the dispatch assumed. None of them would have shown up in a record: all three produce a file byte-identical to a correct dispatch, which is why the suite was green over them. THE PROMPT WAS DISCARDED. A reviewer in this class takes its focus as a POSITIONAL argument and wires stdin only for a different subcommand, so a dispatch that writes the prompt to stdin runs UNSTEERED and exits zero — and the record then attests to a completed review of whatever the agent chose to look at. That is the same defect as the subject never being sent, one level out: the first told the agent what to look for and not what to look at, this told it neither. `prompt_arg` declares the channel, defaulting to stdin so a row that says nothing keeps the landed contract. THE TEST HAD TO ASSERT ON THE INVOCATION, because nothing in the record can show it. `last_call` reads the stub's argv. The assertion is on the subject POINTER rather than a phrase from the prompt — prose is edited freely, and the first version of this case failed against correct code because the prompt's heading is `# Pressure-test` and the assertion looked for `pressure-test`. READINESS IS THE RUNNER'S OWN ANSWER. Inferring "no runner" from a missing file cannot tell a machine that never installed the reviewer from one where it is installed and unauthenticated, and those are different remedies — the operator gets neither. `probe` declares a subcommand whose `ready` boolean decides could-not-look. Conservative in the direction that costs nothing: anything but an explicit `true` reads as not-ready, so a probe that cannot spawn, exits non-zero or answers in an unrecognised shape leaves the review unjudged rather than refusing a branch over an environment nobody can see. Parsed as JSON rather than scanned, so a `"ready": false` beside a remedy string mentioning the word cannot read as true. An undeclared probe skips the whole path. COMPLETION IS THE CONTRACT, NOT A PARSE. The gate refuses ABSENCE and never reads a finding, so demanding Batten's line format from the agent coupled the record to a schema nothing consumes — and made every reviewer that speaks its own into a failed dispatch, leaving no record and refusing the branch for somebody else's stdout. A run that exits 0 records, findings or not; a runner that does emit pointers still gets them stored. `a_runner_that_answers_in_prose_leaves_no_record` is retired rather than deleted quietly: it asserted the behaviour this reverses. What replaces it is sharper — a PERFECTLY PARSEABLE stream with a non-zero exit, which discriminates the two reasons a record can be absent where prose-plus-failure conflated them and passed under either rule. `secrets.rs`' invariant still holds where it belongs: a run that did not COMPLETE records nothing. NOTHING HERE NAMES THE REVIEWER, and the measurement that produced it is CLOUD-472's rather than this crate's (non-negotiable rule 1). No preset, no `[[provision]]` row and no pin: batten does not use that tool, and vendoring a consumer's runner contract into the core is the violation a well-meaning migration makes. Verification: `test:cargo` 3991/3991; `config lint` 0 smells; each new case run by its own name, since a substring filter silently matched four of six earlier in this session and 6/6 green was nearly read as coverage. Refs: CLOUD-1265
…it names `untouched-subject-priced` SURVIVED the sweep: the mutation turns `count(changed) > 0` into `true`, and the case named as its witness passed either way. The reason is the stub's exit code. At 0 the runner writes a record, so `not input.tree.review[id]` is false and the absence arm never holds — with the delta narrowing removed the module still finds nothing to refuse, and the assertion is satisfied by a predicate that stopped reading the delta entirely. At 1 the runner leaves no record, absence holds, and the ONLY thing keeping the tree quiet is that the subject sits outside the delta. That is the conjunct the mutation removes, so the case now reddens when it should. The general form, since this is the second one this session: a case asserting ABSENCE of a finding pins nothing unless every other conjunct is arranged to hold. Green over a mutation is the only way you learn which one was masking. Refs: CLOUD-472
Three claims in the tree said `validator-verdict-clean` decides nothing on any real checkout. All three were false the moment `[tasks.record-verdicts]` landed — an inline mise task `[tasks.verify]` calls BEFORE the gates, which runs `pkl eval hk.pkl` and `renovate-config-validator --strict renovate.json5` and pipes `status clean` / `status error` into `batten record tool` for both rows. None of the three was retired with it, so the row kept advertising a dead gate it had stopped being. THAT COST A COMMIT BEFORE IT COST THIS ONE. Reading those claims, I built an engine-side producer for a decision that already had one — two writers for `.git/batten-tools/<tool>@<version>@<digest>`, disagreeing the moment their argv or their reduction drifts. It is not in this history because it was reverted before the branch was replayed, which is the right place for it: the finding is the prose, not the code. The mise task is also the correct home under house style §5, which makes `check` structurally incapable of spawning — the task says so at its own site, "THE RUN IS HERE BECAUSE IT CANNOT BE IN THE ENGINE." WHAT MOVED, and only the validator half moved. `forge-verdict-required`'s identical sentence is UNTOUCHED because it is still true: nothing in this tree writes `.git/batten-forge/<sha>`. Correcting a claim that still holds because its neighbour's did not would be the same defect mirrored. batten.toml, `validator-verdict-clean` — names the producer and where `verify` calls it, and keeps the absent-is-not-a-refusal half, which is the module's decision and was never the stale part. policy/validator-verdict-clean.rego — the `#MUTANT-OWNER` is WITHDRAWN rather than reworded. Half its reason was the false claim; the other half was real (`tool_verdict_facts.rs` installs a `probe.rego` of its own, so no case in it could redden under a mutation of the shipped module) and is now closed instead of declared. `the_shipped_module_refuses_a_recorded_error` installs `policy/validator-verdict-clean.rego` itself, mints the record with the shipped `batten record tool`, and asserts the refusal; `the_shipped_module_passes_a_recorded_clean` is the discriminating half, without which the first is satisfied by a module that refuses unconditionally. `unclean-verdict-unread` now names the first, and no longer survives the sweep. The fixture declares the row id `config-validator` because that is a literal in the module's own `owned` set. A suite renaming it to something convenient would pass while the shipped selector matched nothing — the same dead gate, one level in. THE HEADER WAS ALSO ORPHANED, earlier on this branch: the `review-dispatched` block went in between that 25-line comment and the `[[rule]]` it describes, so a header about the tool-verdict key's triple has been sitting above the review row. Moved back, in the same admitted write. Refs: CLOUD-1265 Admits: 6c775c003d0da2f954ae062bde4418ea078d765d8694e284dbeeab85f2d9bcfc Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: 037ad82 Admits-epoch: c2d5d2bbf5303c8ff6fefad52befea6726329e569e8baf1e4f00e8e29752d763 Admits-author: alec@wenzowski.com Admits-prev: fbe348673db0a5c9304b0d27bd849b6b89d6b3468d23f1f81f6a738c269bcd6c Admits-answer-lost: A stale claim that a landed deny row decides nothing on any real checkout. Read as a live gap it already cost this session a whole commit building a producer the tree already had, and one of the three sites carries an issue key, so it reads as a live obligation with a tracker row behind it. Also a 25-line header explaining the tool-verdict key left sitting above the review row, documenting a rule it says nothing about. Admits-answer-precondition: The change is prose inside batten.toml's own comment stream: a `validator-verdict-clean` header claiming the row is "inert on a checkout nobody has recorded against, which is every checkout today", which `[tasks.record-verdicts]` falsified when it landed; plus that same header having been orphaned from its rule by the `review-dispatched` block added earlier on this branch. There is no surface for either — a comment has no key, and a block's POSITION relative to its `[[rule]]` is not addressable by any verb. Both land in the diff a reviewer reads. Admits-answer-rejected-route: `config read first` does not apply: I read both sites, and the reading is what established the claim is false — `[tasks.record-verdicts]` runs `pkl eval` and pipes `status clean`/`status error` into `batten record tool config-validator`, and `[tasks.verify]` calls it before the gates. `patch run first` has nothing to patch: no verb writes a comment or moves one.
…by a replay
Two findings `verify` caught that my own `test:cargo` run reported green over.
That run is the first thing to correct: it compiled out of `target/mutate` and
measured 3993 tests where this tree has 4054, so the 3714/3714 I read as a pass
was never a reading of this branch.
THE COUNT IS A MERGE ARTEFACT, and the guard it broke is the one that exists for
exactly this. `Fact::ALL` carries 35 variants — 34 the base already had, plus
`Fact::Review` — while `every_fact_returns_its_stated_const` still asserted 34.
The replay's conflict markers covered the `use` list, where `MINTED` and `REVIEW`
had to be merged by hand; the count assertion sat six lines outside them, so git
took one side silently and neither half of the merge was wrong on its own. The
test's own comment calls itself the anti-vacuity guard "so a DROPPED variant fails
here too rather than quietly shrinking the census" — it caught a GAINED one by the
same arithmetic.
`review` WAS NEVER PLACED, and the table says absence is a refusal rather than an
allow. `module place missing`, which is the arm whose comment records this rule
naming seven unplaced modules before a human did — `patch`, `symbols`, `semver`,
`recorder`, `perf`, `pinned`, `prune`. This is the eighth, and it landed with
clippy green and both test tiers green, which is the property that list is
tracking.
Placed in `symbols`' band because it is `symbols`' class exactly: an acquisition
module that spawns to resolve a `Cost::Effect` fact, whose caller is the engine
deciding WHEN to acquire. `review -> {rules, hook}` is forbidden for the identical
reason, and it binds harder here than for `symbols` — a back-edge would let the
module that dispatches an AGENT reach the engine that decides whether to, and this
spawn costs a round trip where clippy costs seconds.
Neither is a new decision: one restores an assertion to the tree it describes, the
other writes down a placement the module always had.
Refs: CLOUD-472
`config-lint` refused `verdict[prompt run never].override` as an ungroomed weakening, and the right answer turned out not to be grooming it. THE ROUTE COULD NEVER BE SPENT. Its precondition read "the review cannot be dispatched in this environment at all — no runner exists here — rather than not having been dispatched in one where it could". That state is not a refusal: `policy/review-dispatched.rego` reads an absent runner as could-not-look, `a_missing_runner_is_could_not_look_and_never_a_refusal` asserts it over the compiled binary, and `could-not-look-refused` is the declared mutation that pins it. So no case matching the precondition ever produces the verdict the route would override — a hatch for a door that is never shut. That is the same defect this branch has been finding all along, one surface over: a declared thing that reads as live and reaches nothing. It is worth saying that the gate did not find it. `config-lint` reported a WEAKENING, which is a true statement about the bytes and says nothing about whether the route works; reading its own precondition against the module is what found the route dead. The gate asked a question whose honest answer was "this should not exist". WITHDRAWN RATHER THAN GROOMED, and that direction matters: grooming would have added a fourth `Weakens:` clause to CLOUD-472 admitting a hatch nobody can use, and the clause would have read as a considered trade when there was no trade. Removing it is a TIGHTENING, so it owes no clause at all. What remains needs no hatch. `task run first` and `task run other` are command routes, and the state they address — the runner is here, was asked, and left no record — is the branch's own conduct rather than the environment's. Refs: CLOUD-472 Admits: f2c5c32a1efc14250f1e641bf352cbc0a2c31e70465baebddad5bea6fd220459 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: 6194f87 Admits-epoch: 9cfcd8d59c7cf80b7c2b7fdde4c4ef65c0ee585c33b88726ad3ff447e35d2e8c Admits-author: alec@wenzowski.com Admits-prev: fa060cedc1fb70cc975f62de434849db0ba70899a726aaabeb60b1d12794d6d7 Admits-answer-lost: A route that advertises an exit reaching nothing stays in the table. Its precondition is "the review cannot be dispatched in this environment at all — no runner exists here", and `policy/review-dispatched.rego` already reads that state as could-not-look: `a_missing_runner_is_could_not_look_and_never_a_refusal` asserts it over the compiled binary and the `could-not-look-refused` mutation pins it. So no case matching the precondition ever produces the refusal the route would override, and a reader who reaches for it finds a hatch for a door that was never shut. Admits-answer-precondition: The change DELETES a `[[verdict.route]]` block — the `path admit first` override on `prompt run never`. A route is config by construction: no verb writes or removes one, since a gate that could retract its own hatch would be a second authority over the committed table. It lands in the diff a reviewer reads, three lines beside the class it belongs to. Admits-answer-rejected-route: `config read first` is what produced this: reading the class beside the module is how the route was found to duplicate a landed arm. `patch run first` has nothing to patch — no verb adds or removes a `[[verdict.route]]`, which is the property that keeps the override table the committed authority's.
CLOUD-1337. `timer run refused` exempts a backgrounded `sleep` wrapped in `until`/`while`, on sound reasoning: a loop testing a condition exits on the condition rather than on the clock. The exemption asks WHETHER there is a condition and never WHAT it is about — so a loop polling the LOCAL PROCESS TABLE passed, and that is the one condition the harness already reports, since a backgrounded task re-invokes its caller on exit. MEASURED ON THIS CONTAINER, 2026-09-02, by a human reading `ps`: eleven such waits running, the oldest 9h35m, while exactly one real job existed. AGENTS.MD'S CLAIM ABOUT THIS WAS FALSE, and that is the reason this is a gate rather than another paragraph. It has carried the rule since CLOUD-821 with the measurement — "490 in one session, 2 changed a decision" — and the assertion that the shape is "refused by `run-shape-guard`". It was not. The rule was written, the mechanism was named, and the named mechanism let the shape through. An agent read that paragraph, agreed with it, and wrote the wait eleven times in the same session. Non-negotiable rule 2's own case, with the sting that the prose CLAIMED a mechanism it did not have. THE NARROWER RULE WAS DRAFTED FIRST AND WOULD HAVE MADE THIS WORSE. Those eleven were also BROKEN: `pgrep -f` reads full command lines, a mediated call runs as `bash -c` over the whole text, so the pattern was a substring of the polling shell's own command line by construction and the probe matched itself forever. Refusing only the self-match is satisfied by bracketing the pattern (`[m]ise`) — eleven correctly-functioning watchers instead of eleven broken ones, every one still redundant. `a_bracketed_pattern_is_refused_just_the_same` and the `bracket-is-an-exit` mutation are what hold that shut: the fix for a self-match must not be an exit from this gate. WHAT STAYS EXEMPT, so this narrows the form rather than closing it: any condition the harness does not report. `until curl -sf $URL`, a forge status, a file another machine writes — none reaches a process table, none is refused, and `a_wait_on_a_condition_nobody_reports_is_clean` is the anti-vacuity mirror without which every other case is satisfied by a rule that refuses all waits. ITS OWN LOOK-THROUGH RATHER THAN A WIDER `keywords`. A condition segment begins with `until`/`while`, often `!`, and none is in the shared `keywords` set — so `words_program_index` resolved the keyword and the first three cases failed. Widening the shared set would change which program `sleeps` resolves for every landed call, so `condition_program` carries the narrower look-through instead. BOTH ROUTES ON BOTH SLEEP-FAMILY CLASSES ARE QUALIFIED, and there were two rather than the one I expected: each recommended `until <test>; do sleep 1; done` with no word about what `<test>` may read, which is a remedy naming the defect. Refs: CLOUD-1337, CLOUD-821 Admits: c2cf45908086f9e5e0322896d8adaef1fbe68000f3d5567a8300709d43db4aa0 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: a14d387 Admits-epoch: 7878ad9d74a4503f61638dd2d000db2bd6011d9bc77f6e310b0b46254932df6d Admits-author: alec@wenzowski.com Admits-prev: 6bf5c847b2da46a50bf09a9bf2dcd0b8acbda772f279f9cfe80a023b3b2a49e2 Admits-answer-lost: CLOUD-1337 cannot land: `policy/run-shape.rego` fails to LOAD while it raises an undeclared token, so the whole bundle decides nothing — strictly worse than the defect being fixed. And both sleep-family routes keep recommending `until <test>; do sleep 1; done` unqualified, which is the exact shape that produced eleven duplicate watchers for nine hours on this container; a remedy that names the defect is worse than no remedy. Admits-answer-precondition: The change adds the `[[verdict]]` row for `task watch duplicate` with its two routes, and qualifies the `until <test>; do sleep 1; done` target on both sleep-family routes. A verdict class IS config — `policy test` refused the module at load for raising a token no row declares, which is the ABI working — and no verb writes a `[[verdict]]` or edits a `[[verdict.route]]`, since a gate that could declare its own class or rewrite its own remedy would be a second authority over the registry. Both land in the diff a reviewer reads. Admits-answer-rejected-route: `config read first` is what produced this change: reading the sibling rows is how their routes were found to recommend the shape being refused, and there were two of them rather than the one I expected. `patch run first` has nothing to patch — no verb adds a verdict class or edits a route target.
…asons Found by `mutate sweep` after rebasing onto current `main`, which is where both came from: the rows are on `origin/main` verbatim and my earlier sweep predated them. Fixed here rather than filed, because both are in a file this branch already touches and neither needed new test code. `stale-unguarded` SURVIVED, AND ITS CASE COULD NOT REACH THE PREDICATE AT ALL. `a_tree_with_no_wiring_surface_is_not_stale` builds its repo by hand and writes `batten.toml` and the module — but not `policy/harness-declared.json`. `declared` stopped being a Rego constant when CLOUD-1163 made it that document, so in this fixture the table was undefined, `stale` had no rows to iterate, and the mutation on `committed_read > 0` changed no answer. The case asserted a clean tree and would have asserted one over a module that had stopped guarding entirely. That is the exact class `.claude/rules/policy-modules.md` records for a preset reading a `[[pattern]]` row it cannot see: undefined reads as *does not hold*, so the rule decides nothing while everything stays green. Here it arrived through a fixture rather than through a preset. The row the fixture now writes matches nothing wired, which is the state `stale` refuses — so the guard is the only thing keeping that tree clean, and removing it reddens the case. `spent-never` NAMED A CASE THAT IS NOT IN THE DECLARED SUITE. `a_row_whose_owner_has_closed_is_spent` is a `test_` rule in the `.rego`, and `mutate` resolves the declared `#MUTANT-SUITE` path (CLOUD-1267) — so it reported `names-no-case` and the mutation was applied to nothing. Repointed rather than duplicated: `the_engine_reads_a_closed_owner_off_a_minted_receipt` already exists in that suite, mints a real `done` receipt into a real store, and asks the shipped `check` to find it. That is the tier which can see this predicate die; the load-time one fabricates `input.tree.minted` and passes over an engine that never builds it. Refs: CLOUD-1337, CLOUD-1310
…t touch
My previous commit claimed to close `stale-unguarded` and did not. The sweep still
reported SURVIVED, and the reason I believed otherwise is the error worth
recording: I ran the case, saw it pass, and called the fix done. A passing case
says nothing about whether a mutation is observable — that is the whole premise of
declaring one.
`enforced` splits on the pattern's SHAPE. A row carrying a `/` is judged where a
COMMITTED surface was read; a bare basename where a MERGED one was:
enforced(pattern) if { contains(pattern, "/"); committed_read > 0 }
enforced(pattern) if { not contains(pattern, "/"); merged_read > 0 }
`stale-unguarded` mutates the FIRST arm. The row I added was `matches-nothing.sh`
— a basename — so it took the second, which the mutation does not touch, and the
guard died unobserved exactly as before. The fixture had a declared row and still
could not see the thing it was there to see.
Now `mise-tasks/matches-nothing.sh`, which carries the `/` and matches nothing
wired: unmutated, `committed_read` is 0 in a tree with no wiring surface, so the
guard holds and the tree is clean; mutated to `true`, the row is enforced, matches
nothing, and `stale` fires.
SHOWN BOTH WAYS BEFORE COMMITTING, by hand, which is what I skipped last time:
mutation applied → `a_tree_with_no_wiring_surface_is_not_stale` FAILS; module
restored → PASSES. Only the test file changes here; `policy/harness-wiring.rego`
is byte-identical to the commit before.
Refs: CLOUD-1337, CLOUD-1310
…base `land` could not complete a lap: `verify`'s head-of-run `target prune` refused on every one, so no receipt was written and thirteen commits sat unlandable. THE REFUSAL WAS CORRECT AND IT IS NOT ABOUT DISK. `[prune.*.basis]` declared 164 tracked test stems against a live 175 — eleven past a tolerance of 10. Free space was 20006MB against a 17042MB warm floor, so there was never a shortage. The drift is both sides of a rebase rather than one change: CLOUD-472's bundle added `review_dispatched.rs`, CLOUD-1337's added cases, and `origin/main` advanced ten commits under the branch bringing its own. THE HONEST HALF, A THIRD TIME, which is what the block instructs and what the 2026-09-01 and 2026-09-02 entries each did: `count` moves with `measured` and the FLOORS DO NOT. Moving them down needs an independent measurement — a build from an empty `target` for cold, a minimal post-prune tree for warm — which is CLOUD-1158's and which I did not take. A floor too high only refuses laps; one too low fails silently as a rustc IO error inside a test run. AND THE CALLER MISREPORTS THIS REFUSAL, recorded in the block because it cost real time. `verify` says "not enough disk to run the gate, and pruning did not recover it — the refusal above names free space and the floor." The refusal names a STEM COUNT, and free space was 3GB clear of the floor. `target prune`'s stale-basis exit is not its below-floor exit and the caller collapses them, so an operator reading the caller rather than the callee deletes files and gets nowhere. That is what happened here before the callee's own message was read. `mise run target-prune` now exits 0. Refs: CLOUD-1158, CLOUD-1210 Admits: 214320fcb2a8b8664a92a47f868bcd383118bbb53cb5945de6fa0ba54a9588a3 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: d32bdfe Admits-epoch: 6f6e3a71a8067f5899595aba8142e8e0d94abe3257307bc983d4c85d56c2dc5d Admits-author: alec@wenzowski.com Admits-prev: c73a84a3ceafa617abebbbfe8a9d9d3ecf1176bb9a00418e6383b95778ab9a4d Admits-answer-lost: `land` cannot complete a lap. `verify` refuses at its head-of-run prune on every lap, so no receipt is written, `ready-guard` refuses the ready, and thirteen commits sit unlandable. The refusal is correct — the basis was taken against a smaller tree — and the remedy is the count, not the floors. Admits-answer-precondition: `[prune.warm.basis]` and `[prune.cold.basis]` carry `count = 164` against a live 175, eleven past a tolerance of 10, and `target prune` refuses on that. The block states the remedy in its own words — "move `count` and `measured` TOGETHER" — and there is no surface that does it: a basis count is config, and a verb that refreshed its own basis would be a gate rewriting the number it is judged against. It lands in the diff a reviewer reads, beside the block that instructs it. Admits-answer-rejected-route: `config read first` is what produced this: reading the block is how the remedy and its honest scope were found, including that the floors must NOT move without an independent measurement I did not take. `patch run first` has nothing to patch — no verb writes a `[prune.*.basis]` count, deliberately, since that is the number the gate judges against.
…t run is not a pass
CI's `windows` job failed on `ready::a_runner_that_answers_nothing_usable_is_refused`
with `missing escaped value, expected b, e, f, n, r, \, ", x, u, U`. That is a TOML
parse error, not a policy failure.
THE FIXTURE WROTE AN ABSOLUTE PATH INTO A TOML BASIC STRING. `runner = "{}"` with
`D:\a\batten\...\runner.sh` reads `\a` and `\b` as escape sequences. Unix paths carry
no backslashes, so every local run was green — and this was the branch's FIRST real
CI run, since every earlier one was draft-skipped. A TOML literal string fixes it;
hand-escaping the backslashes would be a second authority over TOML's own string
grammar, exercised on exactly one platform.
AND THE SECOND WAVE WAS BEHIND IT. The job cancelled after that one failure with 587
tests unrun, so the suites that drive a `#!/bin/sh` stub had not been reached. Both
would have failed, and the way they fail is the reason this is a gate rather than a
skip: on Windows the spawn fails, the dispatch leaves no record, and ABSENCE IS WHAT
THESE GATES REFUSE. Every refusal case would have passed for the wrong reason while
the one clean case failed — a suite whose negative arms are satisfied by the subject
never running, which is the vacuous pass this family exists to refuse.
`review_dispatched.rs` takes `#![cfg(unix)]` on the module, because every case there
needs the stub. `ready.rs` takes it per case on the five that call
`with_pressure_test`, plus the helper: the rest of that suite reads a Ready block and
needs no subprocess, so a module gate would drop coverage Windows can genuinely give.
Precedent is `bot_lane.rs`, whose own gate records the same shape one client over,
and `session_provisioning.rs` / `connector_allow_door.rs`. A `.cmd` twin of either
stub would be a second authority over what the runner answers.
All five `ready` cases and the review-dispatch suite pass on unix with the gates in.
Refs: CLOUD-472, CLOUD-1337
One blank line. `verify`'s `ci:quick` refused the tree with `the file is not properly formatted`, and the cause is mine: I resolved the `[prune.*.basis]` rebase conflict by hand and did not re-run `mise run fmt` before letting `land` re-verify. THE RULE THE HAND-RESOLUTION SKIPPED: a conflict resolved by editing is a change like any other and owes the same formatter pass. Nothing distinguishes it from a typed edit except that a rebase produced the occasion. ITS OWN COMMIT RATHER THAN AMENDED IN. It belongs to `fix(prune)` two commits back, and reaching that needs a rebase over the Windows fix that sits between — a real risk of dropping work for one blank line. Amending HEAD instead would file a `batten.toml` whitespace change under a message about Windows test gating. AND IT STILL COSTS AN ADMISSION, which is the gate being right rather than pedantic: `protected-mutation` decides on the PATH, not on the size or the authorship of the diff. A formatter wrote this one and the write is still a write. Refs: CLOUD-1158 Admits: 1e307c3d9dcc2ea28e3a6a813a06f1b1c6eef4aa8e3cfb324441c7a9d14686fc Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: f2e8f82 Admits-epoch: 29583962133507378679aa607ee2522af964ed91665eeed73b45285dbb193072 Admits-author: alec@wenzowski.com Admits-prev: 214320fcb2a8b8664a92a47f868bcd383118bbb53cb5945de6fa0ba54a9588a3 Admits-answer-lost: `verify` refuses the tree on every lap, so `land` cannot complete one and eighteen commits stay unlandable. The refusal is correct and the remedy is exactly this one line; there is no smaller change that clears it. Admits-answer-precondition: The change is one blank line, produced by `taplo` through `mise run fmt` rather than typed: my hand-resolution of the `[prune.*.basis]` rebase conflict left a double blank line and `ci:quick` refused the tree with "the file is not properly formatted". The formatter IS the surface here and it ran — what it cannot do is carry its own admission, because the path is protected whatever wrote it. It lands in the diff a reviewer reads as a single deletion. Admits-answer-rejected-route: `config read first` does not apply: nothing about the file's meaning is in question, and reading it is how the stray line was found. `patch run first` is the closest thing to a real alternative and it is what produced this diff — `mise run fmt` is the patch, and it still leaves the write to be admitted.
0e2f700 to
6d68d32
Compare
|
❌ The last analysis has failed. |
|
/fast-forward |
CLOUD-1079's §7 predicted this commit in these words: "the two
stop-hook-git-check.sh / session-start-git-identity.sh rows come out of
the DECLARED table in one commit, with the gate green because the
siblings are genuinely gone."
That is what happened, and the order matters. `session:wiring` removed
both registrations from the merged surface (2 -> 0, with
merged_surfaces_read non-zero so the census is known to have looked).
The exemption rows then outlived what they excused, and
`harness-wiring`'s STALE direction refused the tree — two findings, one
per row.
So the gate went red because the defect was FIXED. That direction is
CLOUD-1314's restoration, which its own header notes the earlier split
had dropped entirely ("no STALE direction over them at all"), and this
is the first time it has had a real subject to catch rather than a
fixture.
The table is `{}` again, which is where it was before #826 repointed the
rows from the closed CLOUD-605 to CLOUD-1079. An empty object and an
absent file are deliberately different readings — the module's own
comment says so — and empty is the right one: this repository declares
no exemption, rather than declining to say.
Refs: CLOUD-1079
Sixteen commits on current
main. The first eight were written onclaude/cloud-1295-retire-bot-issueafter PR #811 was closed mid-session — twosessions were writing that branch at once — and its closing comment said nothing was
left behind. These were: they landed after it and never reached the successor branch.
The rest are this session's.
Closes CLOUD-472
Closes CLOUD-1337
Closes CLOUD-1265
Closes CLOUD-1314
DO-NOT-CLOSE CLOUD-1158
DO-NOT-CLOSE CLOUD-760
DO-NOT-CLOSE CLOUD-858
DO-NOT-CLOSE CLOUD-135
The four closes, and two of them were nearly punted
closing-key-checkis right that the commits SERVED all eight. My first pass declinedsix of them; two of those declines did not survive being checked.
it met four of the five acceptance clauses. The fifth is "a deliberately-broken
subject produces a refusal end to end over the compiled binary; the same subject
repaired produces none" — which its §7 singles out as "the second tier is the one
that matters here." That clause was unmet until this branch:
tool_verdict_facts.rsinstalled only a fixture
probe.rego, never the shipped module, and the row's own#MUTANT-OWNERsaid so — "the tier it names drives the FACT and never thepredicate."
the_shipped_module_refuses_a_recorded_errorand..._passes_a_recorded_cleanare that clause. Closing it.rows are deleted from
declared, andharness-wiringrefuses their return." Nocommit in this repository can meet that: the launcher rewrites both files at every
session start, measured with mtime three hours after the commit that deleted their
rows. Its actual defect — "the row was right to exist and wrong to point at
CLOUD-605", a licence keyed to a closed issue — is fixed here: both rows name
CLOUD-1079, the row that owns the environment half. Re-scoped on the row and closed,
rather than left In Progress against a clause nothing can satisfy.
The four declines are declines on the merits: CLOUD-1158 needs an independent floor
measurement (a build from an empty
targetfor cold, a minimal post-prune tree for warm)that I did not take, and the commit says so; CLOUD-760, CLOUD-858, CLOUD-135 are
Refs:citations of prior art, and CLOUD-135 has been Done since 2026-08-11.What lands
CLOUD-472 — a vendored prompt's dispatch becomes a fact.
Fact::Reviewis thesecond
Cost::Effectoccupant afterFact::Symbols: resolving it runs a program.review.rsfollows the delegated-analyser shapesecrets.rsandsymbols.rsestablished, with its invariant intact — clean is never inferred from a stream that
failed to parse, so every failure arm leaves NO record and a broken runner is
indistinguishable from one that never ran. The record is keyed
id@promptdigest@subjectdigest, so editing the subject leaves the old verdict under aname nothing looks up.
Two subjects, one declaration, and neither gate answers for the other.
review-dispatchedrefuses at landing over a tracked document;ready lintrefuses atthe ready queue over a body that never touches disk. Only the second can see a tracker's
own text, which is why the grooming row carries no
path.Absence is the only arm either gate refuses. A missing runner is could-not-look, not
guilt; a review that reported findings is still a review that ran. Refusing on what the
agent CONCLUDED would be a model verdict wearing an exit code (non-negotiable rule 3).
Three defects found by reading the real runner's contract
Batten does not run the Codex CC companion; customers do. Reading
codex-companion.mjsrefuted three assumptions: the prompt was discarded (focus is positional;
readStdinIfPiped()is wired only fortask), readiness was inferred fromis_file()wheresetup --jsonis the oracle and separates not installed from notauthenticated, and
pointers_indemanded batten's line format from an agent whosefindings the gate never reads.
CLOUD-1265 — the shipped module gets a tier that can see it die
validator-verdict-clean's compiled-binary suite installed a fixtureprobe.regoandnever the module itself, so
unclean-verdict-unreadsurvived every sweep and the rowcarried a
#MUTANT-OWNERconceding it. Two cases now installpolicy/validator-verdict-clean.rego, mint the record with the shippedbatten record tool, and assert the refusal and its discriminating mirror. The fixturedeclares the row id
config-validatorbecause that is a literal in the module's ownownedset — a suite renaming it to something convenient would pass while the shippedselector matched nothing.
Three stale claims said "SILENT UNTIL A PRODUCER WRITES … which is every checkout
today" long after
[tasks.record-verdicts]closed that. Reading them, I built a secondwriter and reverted it; neither is in this history, and the finding lives in the prose
commit. Only the validator half is corrected —
forge-verdict-required's identicalsentence is still true.
CLOUD-1337 — a backgrounded wait that polls a local process is refused
Measured on this container: eleven such waits running, the oldest 9h35m, while
exactly one real job existed.
timer run refusedexempts asleeploop wrapped inuntil/while— but the exemption asks whether there is a condition and never whatit is about, so a loop polling the process table passed. That is the one condition the
harness already reports.
AGENTS.md has carried the rule since CLOUD-821 and the claim that the shape is "refused
by
run-shape-guard". It was not. This makes the sentence true.The narrower rule was drafted first and would have made it worse. Those eleven were
also broken —
pgrep -freads full command lines and a mediated call runs asbash -cover the whole text, so the probe matched itself forever. Refusing only the self-match is
satisfied by bracketing the pattern: eleven working watchers instead of eleven broken
ones, and no less waste.
a_bracketed_pattern_is_refused_just_the_sameand thebracket-is-an-exitmutation hold that shut.Fixes to
mainthis branch could not proceed withoutrevert(hook)—hooks-wiring-checkwas red in every session here, which is thepre-commit gate refusing every commit on a condition no commit can reach. Two
independent causes, and only one of them is still this branch's:
mainreached the[hook] exclusive = truewithdrawal independently under CLOUD-1324, so the rebase leftthat half nothing to apply and its
Admits:block went with it — an admission naming awrite the commit no longer makes is the same declared-but-unreachable class this branch
spent the session closing. What remains is CLOUD-1314's row deletion, which assumed
hooks the launcher rewrites every session.
main's own note needs this commit to betrue: it says the two rows are "therefore back in
policy/harness-declared.json", andon
mainthat file is{}. Its acceptance case is withdrawn rather than deleted —a case removed is indistinguishable from one that never held — with a second case
pinning the direction.
fix(prune)—[prune.*.basis]declared 164 stems against a live 175, solandcould not complete a lap. Not disk: 20006MB free against a 17042MB floor. Recorded in
the block:
verifyreports this as "not enough disk to run the gate", which sends anoperator to delete files when the refusal names a stem count.
Two
harness-wiringmutations that reached nothing, both frommain.stale-unguardedsurvived because its fixture never wrotepolicy/harness-declared.json—
declaredstopped being a Rego constant when CLOUD-1163 made it a document, so thetable was undefined and
stalehad nothing to iterate.Verification
mise run policy-test— 46 bundles, 583 passed, 0 failed.mise run mutant— 11 survivors + 1 could-not-look, from 12 + 2. Every remainingone is pre-existing and owned by CLOUD-845 or CLOUD-989. Four closed:
unclean-verdict-unread,untouched-subject-priced,stale-unguarded,spent-never.a suite of absence assertions is a green that means nothing — measured this session at
6/6 passing with the one positive case never running.
Corrections made in-flight, recorded because each was believed first
test:cargowas first reported 3714/3714 against a contaminatedtarget/mutatebuild; the honest run found two real defects.
stale-unguardedwas first "fixed" with a basename row, which takes themerged_readarm the mutation does not touch. Proven both ways before the second commit.
| tail— the shapeverdict-not-discardedexists to refuse.Refs: CLOUD-1079, CLOUD-845, CLOUD-1310, CLOUD-821, CLOUD-1210
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtDugY1mEeCLNA8ULMHocH