Skip to content

feat(facts): a vendored prompt's dispatch becomes a fact the tree can read - #826

Merged
wenzowski merged 16 commits into
mainfrom
claude/suite-subject-retirement-g2g00c
Sep 2, 2026
Merged

feat(facts): a vendored prompt's dispatch becomes a fact the tree can read#826
wenzowski merged 16 commits into
mainfrom
claude/suite-subject-retirement-g2g00c

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Sixteen commits on current main. The first eight were written on
claude/cloud-1295-retire-bot-issue after PR #811 was closed mid-session — two
sessions 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-check is right that the commits SERVED all eight. My first pass declined
six of them; two of those declines did not survive being checked.

  • CLOUD-1265 — I declined it as "PR feat(facts): build the verdict-store producer, and retire three wrappers into the rows it wakes #797 owns the producer." feat(facts): build the verdict-store producer, and retire three wrappers into the rows it wakes #797 is merged, and
    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.rs
    installed only a fixture probe.rego, never the shipped module, and the row's own
    #MUTANT-OWNER said so — "the tier it names drives the FACT and never the
    predicate."
    the_shipped_module_refuses_a_recorded_error and
    ..._passes_a_recorded_clean are that clause. Closing it.
  • CLOUD-1314 — its stated Done is "both hooks are gone from the harness config, the
    rows are deleted from declared, and harness-wiring refuses their return."
    No
    commit 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 target for 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::Review is the
second Cost::Effect occupant after Fact::Symbols: resolving it runs a program.
review.rs follows the delegated-analyser shape secrets.rs and symbols.rs
established, 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 a
name nothing looks up.

Two subjects, one declaration, and neither gate answers for the other.
review-dispatched refuses at landing over a tracked document; ready lint refuses at
the 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.mjs
refuted three assumptions: the prompt was discarded (focus is positional;
readStdinIfPiped() is wired only for task), readiness was inferred from
is_file()
where setup --json is the oracle and separates not installed from not
authenticated
, and pointers_in demanded batten's line format from an agent whose
findings 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 fixture probe.rego and
never the module itself, so unclean-verdict-unread survived every sweep and the row
carried a #MUTANT-OWNER conceding it. Two cases now install
policy/validator-verdict-clean.rego, mint the record with the shipped
batten record tool, and assert the refusal and its discriminating mirror. 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.

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 second
writer 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 identical
sentence 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 refused exempts a sleep loop wrapped in
until/while — but the exemption asks whether there is a condition and never what
it 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 -f reads full command lines and a mediated call runs as bash -c
over 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_same and the
bracket-is-an-exit mutation hold that shut.

Fixes to main this branch could not proceed without

revert(hook)hooks-wiring-check was red in every session here, which is the
pre-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: main reached the
[hook] exclusive = true withdrawal independently under CLOUD-1324, so the rebase left
that half nothing to apply and its Admits: block went with it — an admission naming a
write 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 be
true
: it says the two rows are "therefore back in policy/harness-declared.json", and
on main that 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, so land
could not complete a lap. Not disk: 20006MB free against a 17042MB floor. Recorded in
the block: verify reports this as "not enough disk to run the gate", which sends an
operator to delete files when the refusal names a stem count.

Two harness-wiring mutations that reached nothing, both from main.
stale-unguarded survived because its fixture never wrote policy/harness-declared.json
declared stopped being a Rego constant when CLOUD-1163 made it a document, so the
table was undefined and stale had nothing to iterate.

Verification

  • mise run policy-test — 46 bundles, 583 passed, 0 failed.
  • mise run mutant11 survivors + 1 could-not-look, from 12 + 2. Every remaining
    one is pre-existing and owned by CLOUD-845 or CLOUD-989. Four closed:
    unclean-verdict-unread, untouched-subject-priced, stale-unguarded, spent-never.
  • Every new case run by its own name. A filter matching fewer tests than intended over
    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:cargo was first reported 3714/3714 against a contaminated target/mutate
    build; the honest run found two real defects.
  • stale-unguarded was first "fixed" with a basename row, which takes the merged_read
    arm the mutation does not touch. Proven both ways before the second commit.
  • A formatter's exit status was discarded through | tail — the shape
    verdict-not-discarded exists to refuse.

Refs: CLOUD-1079, CLOUD-845, CLOUD-1310, CLOUD-821, CLOUD-1210

🤖 Generated with Claude Code

https://claude.ai/code/session_01VtDugY1mEeCLNA8ULMHocH

@linear-code

linear-code Bot commented Sep 2, 2026

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

Why

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

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

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

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

The failure is structural, not inattention. Reconstructing it:

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Subagents are a producer, never the judge

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


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

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

  • **Authority boundary (§1). **crates/batten/src/ready.rs (the dialect decision in check_claims and its caller), the consumer-declared cutover in batten.toml, and a compiled-binary tier in crates/batten/tests/it/ready.rs. No change to the claims grammar, to REQUIRED_CLAIMS, or to check_claimed_tests — those already say the right thing and are simply unreachable.

  • Computable predicate (§2). A Ready block carrying no claims object is a finding when the row was created at or after a consumer-declared date. Before that date the prose dialect passes exactly as today. The comparison is the payload's own createdAt against a fixed-width ISO-8601 literal, so it resolves with no network call and no second read of the tracker.

  • A RATCHET, NOT A FLIP, AND THE MEASUREMENT IS WHY (§2). Flipping it unconditionally refuses the entire Todo queue in one step — 50 rows, and the sample carries no object at all — while graph-check enforces Todo ⇒ ready-lint exits 0, so the board's whole ready frontier would go dark at once. That is CLOUD-858's shape, where three rows made graph-check refuse the whole board. Grandfathering by creation date closes the escape going forward and costs the standing queue nothing.

  • Could-not-look is not a refusal (§2). A payload with no readable createdAt leaves the row judged exactly as today. A verdict about a missing field is not a verdict about the row, which is the three-valued posture the rest of this engine already takes.

  • The date is the CONSUMER's (§2). It is a fact about this repository's own board, so it lives in batten.toml and never in crates/batten — non-negotiable rule 1, the same reason the tracker's vocabulary is not the crate's to name.

  • Deliberately not in scope (§2). Binding a declared tests[].file to a case in the tree and holding a declared mutation to the sweep — that is increment 2, and it is the half this row was originally filed for. Changing what REQUIRED_CLAIMS demands. Migrating any existing row's body. Retiring the prose path outright.

  • **Effect (§3). **read.

  • Output and exit (§5). The one contract: 0 clean, 1 the finding, 2 could-not-look. Pointer-only — the block's line and the rule id, never a clause's text.

  • **Commit / bump (§6). **feat(ready) — patch (0.0.x).

  • Test obligation (§7). Over the compiled binary in crates/batten/tests/it/ready.rs. Shown able to fail, four discriminating observations: a post-cutover row with no object is refused; a post-cutover row WITH an object is clean; the anti-vacuity mirror — a pre-cutover row with no object is clean, without which the arm is satisfied by a check that refuses every prose block; and an unreadable createdAt leaves the row judged as before.

  • Blockers (§8). None. relatedTo CLOUD-453 (which built the claims object this makes reachable), CLOUD-418 (the mutation field), CLOUD-1311 (the sibling escape — a nudge with no exit code, where this is a mechanism with no reachability), CLOUD-858 (the measured cost of refusing a whole board at once), CLOUD-369 (the instance).

  • Weakenings this row admits (house style §8) — GROOMED AFTER THE WORK, AND THAT IS THE FIRST THING TO SAY ABOUT THEM. The six pairs below were not foreseen at refinement. Increment 1 was scoped as a dialect ratchet, increment 2 grew inside the same branch, and the config surface each new gate needs was discovered while building rather than while refining — so the independent check this clause exists to provide did not happen for them. A reviewer should read what follows as the author's account of his own change, not as a prior decision it conformed to. config-lint refused the branch and the refusal was correct.

    Three new [[verdict]] classes, each arriving with an override **route. **test name undefined (increment 2's core: an obligation naming no case, or a case no mutation can kill), plan declare held (an entry a branch declared and neither finished nor withdrew) and issue file held (a row the branch filed that nothing closes). A class that declares an override is a gate that ships with a hatch, and the lint reports that as a weakening whether or not the class is new — deliberately, because whether one precondition is looser than another is a judgement it refuses to make.

    What is given up: all three are admittable from the day they land, so each refusal can be spent rather than only obeyed, and the spend costs one articulation instead of the work. What is not given up, and is why each hatch is there rather than in spite of it: every one of the three has command routes FIRST, and each precondition names a condition the author cannot fake into existence — an obligation discharged by a successor the [rule.conserves] ledger already declares, an entry needing a decision or artifact that does not yet exist, a filed row that is not this branch's deferral. Without a route, the honest cases have no exit and the gate teaches people to route around it, which is the failure mode this repository has measured more than once.

    • **Weakens: **verdict-override-added at verdict[test name undefined].override
    • **Weakens: **verdict-override-added at verdict[plan declare held].override
    • **Weakens: **verdict-override-added at verdict[issue file held].override

    An obligations column on all three board-writes **recorders. **recorder-changed is the sharpest key on the weakening table because it is indirection: a recorder can be byte-identical while what its columns resolve to moves, and the record reaching the gate then carries whatever that resolution says. The column is on all three rows rather than one because the readers index by row and a column present on only some of them is a channel that answers for some writes and is silent for others — which reads as a clean record rather than as an unread one.

    What is given up: the three records now carry a field nothing on main produced, so a reader comparing records across the merge sees a shape change and must not treat the pre-merge absence as an empty obligation set. What is not given up: no existing column's resolution changed, the addition cannot make an existing record say less, and the declared set still reaches the tree over the description the TRACKER returned rather than a payload the caller assembles — the forgery control the verdict column already carries, earned when ready-lint over a self-assembled payload was measured green three times against text in a local file.

    • **Weakens: **recorder-changed at recorder[board-comment]
    • **Weakens: **recorder-changed at recorder[board-issue-created]
    • **Weakens: **recorder-changed at recorder[board-issue-groomed]

Acceptance

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

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

CLOUD-1265 Nothing WRITES a `tool-verdict` record, so `validator-verdict-clean` decides nothing on any real checkout — build the producer verb, and park three retirements (38.3s) behind it until it exists

Why

CLOUD-1171 landed the READ half of the third-party-validator fact and it is correct.
crates/batten/src/tools.rs resolves .git/batten-tools/<tool>‖<version>‖<digest> through
record_key/record_path/verdicts, keyed by the triple that makes a record stale by
construction, and policy/validator-verdict-clean.rego adjudicates what it finds — three
answers kept properly apart (null = could-not-look, absent = no record under this key,
present-and-empty = the tool ran and found nothing).

Nothing in the tree WRITES one. Checkable rather than asserted:

git grep -l 'batten-tools' -- crates/ policy/ mise-tasks/ .github/
#   crates/batten/src/tools.rs          <- the reader
#   crates/batten/tests/tool_verdict_facts.rs   <- a TEST, the only writer

So on every real checkout input.tree["tool-verdict"] resolves null, the module's
is_object guard declines, refused is empty, and the gate is byte-identical to a clean**
**tree on the decision surface. That is CLOUD-845's dead gate, shipped.

The load-time tier cannot see this, which is why it shipped green

policy/validator-verdict-clean.rego's own test_could_not_look_does_not_fault asserts
count(violation) == 0 with input as {"tree": {"tool-verdict": null}} — and passes,
correctly, over a repository where null is the permanent state. .claude/rules/policy-modules.md
names this exact failure: "a with input as case … fabricates the very shape the engine may*
be unable to produce, so it passes over a key nothing fills and over a channel nothing
*populates."

It is also the field instance that file records one level out: OpenTelemetry's weaver
printing ✔ No 'after_resolution' policy violation, exit 0, over a knowingly-broken registry,
because the module read a key the schema never built. Same shape, same silence, same green.

What it costs, in seconds that are otherwise landable today

Three retirements have no other blocker and cannot move until a producer exists. Measured
against bench/suites/RESULTS.md at origin/main:

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.rs gains the write path; the verb is
    registered in crates/batten/src/surface.rs. policy/validator-verdict-clean.rego is
    unchanged — it is already correct and is the consumer, not the defect. No
    mise-tasks/ program and no tests/**/*.bats is added or edited.

  • Computable predicate (§2). After a producer run over a declared [[rule.tool]] row, a
    fresh batten check on the same tree resolves input.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, where perf already 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 prosetools.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 until 0.1.0.

  • Test obligation (§7). Over the compiled binary, and the second tier is the one that**
    **matters here — a with input as case is what let the dead version ship. Three
    discriminating observations, per CLOUD-418: the producer writes a record and a subsequent
    check REFUSES 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 blocks the three retirements above. relatedTo CLOUD-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 no conserves arm).

  • Weakens (§8), groomed late and recorded as such. The producer's caller is a new inline mise.toml task body, which inline-task-bodies-not-growing counts (31 -> 32) and denies. The two dispositions its own no_fix_reason names are "migrate the predicate onto a rule kind, or waive the increase deliberately with a reason and an expiry" — and the predicate did migrate: what pkl-check decided is policy/validator-verdict-clean.rego's now, off the config-validator row. 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.sh and its 76-line suite leave the tree in the same commit, so the bash surface falls while a mise.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, since base = "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-lint refused the change — the same sequence CLOUD-1164 records for its own Weakens clause. The alternative (collapsing the task body to a one-line shim) was examined and declined: it drops the /root/.ccr/ca-bundle.crt fallback carried verbatim out of the retired program, which is the arm this container actually takes, so a valid hk.pkl would fail as though malformed.

    **Weakens: **waiver-added at waiver[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-clean gains a sibling [[rule.tools]] row (renovate-config, renovate-config-validator over renovate.json5) beside the landed config-validator row. config-lint reads any edit to an existing rule's predicate as rule-predicate-changed and refuses it against origin/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-profile in 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-profile is 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-lint armed against origin/main refused it under verify. An earlier unarmed run had reported 0 smell(s), which is what let it reach verify at 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-changed at rule[validator-verdict-clean].tools

Acceptance

  • batten tools record exists, 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:38launcher-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's merged_siblings is the observation this repository can make.
  • **Computable predicate (§2). **batten doctor hooks -J reports merged_siblings: 0 for claude-code on a fresh container, with merged_surfaces_read non-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 until 0.1.0, for the follow-up that removes the two now-obsolete DECLARED rows. Not chore: release-plz gives chore no 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 two stop-hook-git-check.sh / session-start-git-identity.sh rows come out of hooks-wiring-check.sh's DECLARED table 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, which tests/hooks-wiring-check.bats already drives through HOOKS_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_tree already exists (git.rs:784) and CLOUD-833 already uses it for bundle membership under --config-from. Emit input.tree.tracked from it, bounded the way documents is (declared, never an ambient walk) or the read classification 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 test refuses a with input as whose keys the engine cannot produce. The input document's shape is already derived rather than designed — CLOUD-834 makes its keys the Fact variants, 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_document is 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 one tree_document builds from — named once, not restated in the test harness.
  • Computable predicate (§2). Three, each decidable: (a) input.tree.tracked is emitted, from git::list_tree, bounded by declaration; (b) every field the module doc's examples reference exists in what tree_document emits — an assertion over the doc text against the emitted key set, the shape spawn_census.rs:216 already uses against clippy.toml; (c) batten policy test refuses a with input as naming a key outside the emitted set, at exit 1 (a config fault, not a policy verdict).
  • Effect (§3). read. list_tree is the same bounded, fixed-argv git query bundle_members already makes. No new verb, no Authority change.
  • Generated artifacts (§4). schema/batten.schema.json if a row key changes; drift-gated by schema-check.
  • Output & exit (§5). Pointer-only: the offending key and the module path, never the module body. Exit 1 for the refusal in (c) — it is a config fault at load, not a violation, per the one table.
  • Commit / bump (§6). fix(policy)patch until 0.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 passed and denies nothing. (a) that same pair goes red after the fix — either the predicate fires or the test is refused; (b) a module using input.tree.tracked against a fixture with a matching tracked file denies; (c) a with input as naming 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. relatedTo CLOUD-833 (which built tree_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 whose with input as names 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 failed beside a silent check.

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 subprocessgit.rs:784 uses open / rev_parse_single / peel_to_tree / lookup_entry_by_path under gix::open::Options::isolated. The §3 effect clause reasoning from "the same bounded, fixed-argv git query bundle_members already 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 is policy.rs:982's bundle membership under --config-from, which wants the files in one folder. A tracked fact 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::Format does not know — checked at rules.rs:2917-2923 before any I/O, so CLAUDE.md, hk.pkl and any .bats suite 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(...) else at rules.rs:2924, which its two sibling acquisition sites (:3962, :4110) do distinguish;
  • a parse failure — Look::IsNot merged with Look::CouldNotLook at rules.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 log is 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.rego plus a compiled-binary tier in crates/batten/tests/it/. No change to what run-shape-guard's surviving families decide.
  • Computable predicate (§2). Two arms, both over a mediated call carrying a while/until keyword. Family 1: a segment whose effective program is pgrep/pkill/ps/jobs, or kill -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; done is 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; done is 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. relatedTo CLOUD-489the 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. Also relatedTo CLOUD-821 (the measured 490-wakeups finding this is the unbounded cousin of), CLOUD-1163 (which retired run-shape-guard.sh onto policy/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.sh keeps 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 no until <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). **Floor and Floor::validate in crates/batten/src/prune.rs, and the [prune.warm] / [prune.cold] tables in batten.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.sh no longer exists (retired whole under CLOUD-1059); mise-tasks/** and tests/**/*.bats are refused by V-SHELL-RULE-EDITED with no override and no bypass_env.

  • **Computable predicate (§2). **Floor gains 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 since measured — 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:1038 calls prune.validate() from the shared config-load path, which **every **batten invocation runs, batten hook on every mediated tool call included. A glob walk there is taxed onto the PreToolUse path against perf-assert's ceiling, and batten check over 654 tracked files already costs ~3.6 ms above the process floor. The repository has ruled on this exact shape once: claim-race-check was moved off the mediated call because a round trip on every tool call is disqualifying, and RuleKind::scopes pairs every spawning kind with RuleScope::Tree alone.

    So: Floor carries the declared basis (it is config, and validating its shape at load is free), and the comparison against the live tree runs in batten target prune — already Cost::Effect on Surface::VerifyOnly, which perf-assert deliberately budgets no ceiling for — or as a tree-scoped rule row under verify. Either is acceptable; Floor::validate is 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 measured date. 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 measured already 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. relatedTo CLOUD-861 (the ratcheting floor this keeps satisfiable), CLOUD-1157 (the reclaim that sets what size × stems actually 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 hook does not walk the tree for this, and perf-assert is 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 in crates/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:

  • command rule 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 check is Effect::Read and run_static (rules.rs:1544) refuses any spawning kind outright, exit 1. If an effect-class fact were resolvable under check, that promise weakens.
  • The honest options are (a) effect-class facts are available under enforce only, mirroring how spawning kinds are already confined; (b) check gains 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: verify already writes SHA-keyed receipts that hook reads (CLOUD-203). A fact resolved once at verify time and read as data later is exactly the amortisation this milestone is about, and it keeps check structurally 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_path is 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::Command vs std::process::Command case 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:line and 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 check either 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.rs reads ripsecrets' output as colon-delimited textparse_line (:1286-1307) is split_once(':'), a number.parse(), and a longest-prefix path match, and SCANNER_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=json from 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 new Fact variant, the first occupant of Cost::Effect, with its class() const beside the existing ten and its tree_key() in the same table. Appended, never inserted. The acquisition generalises secrets.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::Command sites 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 separate clap::Command from std::process::Command has resolved nothing, and surface.rs's bare use 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::Effect means resolving it spawns a program and means only that (facts.rs:96). Surface::Hook is refused: run_static already refuses a spawning kind outright, and this fact must not weaken that promise. Whether check may 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.json and schema/batten.local.schema.json regenerate for any declaration key the fact adds. Regenerate with mise run fix; never hand-merge a generated diff. derived-check and schema-check gate 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:line and 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 until 0.1.0. Not breaking for the library surface: the variant is appended and no existing Fact arm moves, which is what mise run semver measures.
  • 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 discipline crates/batten/tests/facts.rs already applies to the ten variants.
  • Blockers (§8). None. CLOUD-757 is Done, and the relation this row carried has been dropped rather than left to read as live. blocks CLOUD-762 and CLOUD-914. relatedTo unchanged.

Acceptance, restated against the re-founding

  • A Fact variant occupies Cost::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::Hook cannot 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:1479outputs::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:1544run_static refuses any spawning kind unrelated run_static is declared at rules.rs:2928
secrets.rs:1013-1016parse_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:363canonical_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 check resolves this Effect × Check fact directly or consumes a receipt-backed fact.
  • Probe: generalize the recorded secrets.rs parse/exit reconciliation shape for clippy structured output and run the stated spawn census over the existing tree, including surface.rs's clap::Command import.
  • 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-lint exit 0. Its mechanism was already specified and its preconditions are real blockedBy relations 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 valued unknown, 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 retired feat → 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-check over 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-lint green 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 needs mise run board-payloads.

Two Todo rows outside this row's scope were also unmilestoned and would have failed graph-check's todo-unmilestoned clause 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, and mem:workflow/board-states classifies 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 → minor is also a retired arrow, and ready-lint cannot see it because the label is not Commit / 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's todo-not-ready) is already landed. What is missing is refinement, not machinery.
  • Predicate (§2). mise run graph-check, piped the payloads for the full Todo closure, exits 0. Equivalently and per row: mise run ready-lint exits 0 for each of the three, OR the row is no longer in Todo. A command and an exit code over payloads the tracker returned; nothing here is judged by reading.
  • Effect (§3). read with 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, which mem:workflow/board-states treats as permission-bearing where a content edit is not.
  • Output and exit (§5). Unchanged. graph-check already 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_issue and recovered byte-identical by mise run board-payloads, never re-typed:
    • mise run ready-lint exits 0 for CLOUD-275 and CLOUD-134, and for CLOUD-472 either exits 0 or the row is out of Todo.
    • mise run graph-check over the whole Todo closure 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-lint exits 0 on, or is out of the Todo column — decided per row, with the reason recorded on that row.
  • mise run graph-check over the full Todo closure exits 0, which it cannot today.
  • CLOUD-134's feat → minor arrow 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

  1. Closed payload-class taxonomy. A judge payload is built from exactly three classes: rule (the judge row's own committed id and criteria text from batten.toml), content (bytes of working-tree files matched by the row's glob), and pointer (path:line spans, 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.
  2. Protected bytes never cross. A matched file that is a member of the resolved protected set (PathSet::contains — the same membership batten 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.
  3. 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.
  4. Cap and channel. Per-row max_payload_bytes, engine default 16 384 (the precedent of the argv-batching bound MAX_FILES_BYTES), tighten-only under the §8 raise-only clamp — a batten.local.toml may 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.
  5. 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 protected set, checked before any file content is read; one protected member refuses the invocation with a pointer-only diagnostic.
  • max_payload_bytes semantics 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 criteria and the matched file's bytes, and negative for three planted sentinels (environment variable, unmatched file, protected file).
  • A payload exactly at max_payload_bytes assembles; 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. The protected set stays the consumer's committed list in batten.toml (already resolved by Sets::from_config); this issue adds no second copy of it.
  • Computable predicate (§2). Not expressible as a batten.toml rule: 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 by PathSet::contains per matched path, cap admission by byte comparison. No inference anywhere. The gate is the fixture suite in mise run test:cargo, an hk gate step run by mise 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. relatedTo CLOUD-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

  1. 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.
  2. SHA-256 is pinned for record byte-stability; the property needed is collision resistance, not the specific algorithm.
  3. 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.records is the [[recorder]] channel and reaches input.facts on the mediated surface, which a tree-scoped rule never sees.
  • batten.toml's issue-read receipt already records {id} {updatedAt} {now} {digest:description} {slug:status}the status is captured — but it is a receipt row, read at the hook boundary, not projected onto the tree document.
  • input.tree.captured is 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 in batten.toml and in consumer modules, never in crates/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 until 0.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. relatedTo CLOUD-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.rego regains 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 reads tool_input.run_in_background; .claude/settings.json plus hooks-wiring-check's DECLARED table for the registration. No new task, no second guard.
  • Mechanism as a computable predicate (§2). Two parts.
    1. Registration. A PreToolUse entry on matcher Bash in .claude/settings.json pointing at $CLAUDE_PROJECT_DIR/mise-tasks/run-shape-guard, plus its mise-tasks/run-shape-guard <this-key> row in hooks-wiring-check's DECLARED (:105-116). Without the row the entry is wiring-sibling-command; without the entry the row is wiring-declaration-stale. Both directions already gate, so the registration is decided by an existing check rather than by review (CLOUD-713/777).
    2. The exemption narrows from "run_in_background is set" to "run_in_background is set and the stage resolving to sleep is inside an until/while loop". Judged per stage after the existing heredoc-drop and quote-scrub, over the already-scrubbed element, using resolve() and the elements[]/seps[] split the file already has. A predicate over the scrubbed element, not a new parser.
  • Blocker to clear in the same change (§8, a precondition rather than a dependency). hook-pin-check refuses 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-51 and :197 read the payload with jq. 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 what payload-field serves, so claiming it here is claiming the wrong exemption.
    • register as mise run -q run-shape-guard — unblocked (hook-pin-check does not judge mise run registrations), but pays ~203ms of task-runner startup on every Bash call, which is the hot path CLOUD-479/435 measured and moved three hooks off.
    • payload-field for all three reads. command and hook_event_name are already served. run_in_background is 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's run_in_background." The enum is #[non_exhaustive] and its doc states "Growing it is a deliberate edit here, which is the point", so adding Field::RunInBackground is 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 name Envelope::input, and a bool cannot carry a secret.
  • Effect (§3). run-shape-guard is a PreToolUse guard reading a payload on stdin; it adds no command to the surface, so no effect declaration and no change to the derived read-only allowlist. batten payload field run-in-background is 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, honours BATTEN_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), and mise 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) for Field::RunInBackground → non-breaking (#[non_exhaustive]); mise run semver decides, not this paragraph.
  • Test obligation (§7). tests/run-shape-guard.bats, in the suite's existing idiom (:14-25: synthesize with jq -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-substring row beside :44-45, since MUTANT_GATES (mise.toml:316) already lists this task and a new family with no row fails no-mutant-declared.
    • THE MEASURED SHAPE: — backgrounded sleep 590; tail -6 /tmp/land.log is denied. Pins this incident, matching the naming convention at :39 and :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; done stays allowed; until curl -sf …; do sleep 5; done stays allowed; a backgrounded long-running command with no sleep stays allowed; a foreground command with no sleep stays allowed; sleep inside a quoted span or heredoc is untouched.
    • The denial names the remedy — assert the text contains mise run alive and the exit-notification sentence, not merely that it refused.
    • Wrapper trio (gh-guard.bats:105-125 convention): allow is silent, fails open on unparseable input, honours BATTEN_RUN_SHAPE_BYPASS=1.
    • mise run hook-pin-check green with the guard registered by path — the blocker, asserted rather than remembered.
    • mise run hooks-wiring-check green — the DECLARED row 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 per AGENTS.md:147 it is committed-and-pushed, not activity, that survives a reclaim. Verify AGENTS.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-guard is registered and fires: a Bash call carrying sleep 590; tail -6 /tmp/land.log with run_in_background: true comes back refused, with text naming mise run alive and the exit notification. Behavioural, end to end — not "the bats suite passes".
  • hook-pin-check and hooks-wiring-check are 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/deps holds 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 **verify lap consumes 25403 MB, reported by target-prune's own
    lap-close line: "the lap opened on 43fc2414 consumed 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-prune closes 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-level crates/batten/tests/*.rs, zero [[test]] entries in crates/batten/Cargo.toml, and no crates/batten/tests/*/main.rs. The grouping §3 specifies was never done.
  • **The ratchet was never written. **batten.toml carries tests-not-deleted and bats-tests-not-deleted, both token counts of #[test] occurrences; no policy/*.rego module refuses an added top-level crates/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 in target/debug/deps. NOT git ls-files 'crates/batten/tests/*.rs', which this clause specified until 2026-09-01: git's default pathspec * matches /, so that form also matches crates/batten/tests/common/mod.rs — a file common/mod.rs:19-21 states is deliberately not a target — and reports one target too many. prune.rs:262-269 is the prior reading and the one this row must not contradict silently.

  • Mechanism (§3). Group the files under crates/batten/tests/<group>/ with a main.rs declaring one mod per former file. Cargo compiles a tests/ subdirectory as a single target exactly when it holds a main.rs — a fact tests/common/mod.rs:19-21 already states and relies on, which is why common/ 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. ratchet is an existing rule kind (CLOUD-55) and tests-not-deleted is 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 a policy/*.rego module **plus a **crates/batten/tests/*.rs tier, and .claude/rules/policy-modules.md states that tier "is crates/batten/tests/*.rs and never a .bats". So every retirement adds a top-level test file by mandate — which is exactly why prune.rs recorded 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 mod inside 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 test is 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-deleted must 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.added contains a path matching crates/batten/tests/*.rs at the top level — one segment past tests/.

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.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 2 minutes.

Check out review usage here.

View limit details

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

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: de796eb3-0daf-4136-a2c6-8b66c67b204b

📥 Commits

Reviewing files that changed from the base of the PR and between 980cdee and 6d68d32.

📒 Files selected for processing (14)
  • .serena/memories/core.md
  • batten.toml
  • crates/batten/src/config.rs
  • crates/batten/src/hook.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/ready.rs
  • crates/batten/src/rules.rs
  • crates/batten/src/trust.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/ready.rs
  • crates/batten/tests/it/review_dispatched.rs
  • mise.toml
  • policy/module-layering.rego
  • schema/batten.schema.json
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: c589c26f-b2b4-4df1-b002-cdfcf8f6e664

📥 Commits

Reviewing files that changed from the base of the PR and between 6194f87 and 980cdee.

📒 Files selected for processing (5)
  • batten.toml
  • crates/batten/tests/it/harness_wiring.rs
  • crates/batten/tests/it/run_shape.rs
  • policy/harness-wiring.rego
  • policy/run-shape.rego
🚧 Files skipped from review as they are similar to previous changes (1)
  • policy/harness-wiring.rego

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


📝 Walkthrough

Walkthrough

Added 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 review-dispatched policy and verdict vocabulary. Added integration coverage for dispatch, caching, failures, cutovers, verdicts, and prompt delivery. Restored launcher hook declarations and updated hook-layering policy.

Merge Risk: ⚪ Minimal · up to 980cd

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 Free

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

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

@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch 3 times, most recently from 17bcaeb to 6194f87 Compare September 2, 2026 14:29

Copy link
Copy Markdown
Contributor Author

The premise the revert rests on is false, and it is measurable today

This PR withdraws [hook] exclusive = true and restores CLOUD-1314's two declared rows, justified by quoting batten.toml's own note — "launcher-provisioned and rewritten at every session start, so no commit here can clear them" — and concludes that a per-surface exclusive is "strictly better than waiting for a zero no commit can produce."

That zero is producible, and the mechanism has been in the tree the whole time.

The measurement this branch could not take

Measured on this container, 2026-09-02, and the reason it reads differently here is the reason the whole class was invisible:

binary siblings merged_siblings ok
0.0.121 (installed, built Aug 28) 0 absent true
0.0.137 (source, Cargo.toml) 0 2 false

The installed binary predates CLOUD-1314, so test_the_retired_launcher_hooks_are_refused_if_they_return (policy/harness-wiring.rego:576) was not in it, and batten doctor hooks -J answered green over both registrations while merged: 2 showed it looking straight at them. Any session that measured with the installed batten got a false reading. This is CLOUD-1326's class, and the engine's own contract-drift advisory fired here saying so: "this session's SessionStart registration did not run … every mediated call until it appeared failed open and said nothing. This is a provisioning failure rather than a policy one."

batten wiring reclaim produces the zero

It is in crates/batten/src/wiring.rs:293, declared at crates/batten/src/surface.rs:3466, and crates/batten/tests/it/wiring_reclaim.rs:173 already asserts the surface no longer contains session-start-git-identity after it runs. On 0.0.137, --dry-run on this container reports:

would remove 2 sibling registration(s) across 1 surface(s) read
claude-code:SessionStart would remove 1
claude-code:Stop would remove 1

It removes registrations, never files — which is exactly why it is the right instrument and why the core.hooksPath blast-radius concern in CLOUD-1079 is a precision requirement rather than a blocker.

Why "no commit here can clear them" was never true

The rewrite happens every session, so a one-off repair loses. A repair that also runs every session does not — and this repository already does that twice. session:signing exists for precisely this reason, in its own words: "the launcher writes commit.gpgsign true --global every session, and local beats global only if something writes local." session:identity is the same shape. The launcher-hook case is the third member of that family and nobody had registered it.

One correction to a fact in the PR body: both scripts carry mtime 16:59 today — rewritten mid-session, not only at session start. That makes an in-session manual delete even more hopeless than recorded, and a session-start handler correspondingly more clearly the answer.

What I'd suggest, rather than asking you to change this PR

The three retirements that reached the committed surface stand, and the revert unblocked a red pre-commit gate — that half was right. What is worth not landing is the restoration of the two declared rows, because they re-establish the exemption that made the census green over the defect, and they cite a sentence that is being retracted.

CLOUD-1079 is corrected and pulled to Todo with the retraction and the trace; CLOUD-605 — the root sentence six rows inherited by quotation — now carries a retraction header. The session:wiring task and its [[hook.handler]] row are what I'm landing. If they land first, this PR's exclusive flip becomes satisfiable rather than withdrawn.

Happy to rebase around whichever order suits you — flagging it now because this PR is behind and draft, and the two changes touch the same declaration.


Generated by Claude Code

@wenzowski
wenzowski marked this pull request as ready for review September 2, 2026 18:40
@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch from 980cdee to 8f83070 Compare September 2, 2026 18:40
@wenzowski
wenzowski marked this pull request as draft September 2, 2026 18:55
@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch 3 times, most recently from 6c76c0f to 0e2f700 Compare September 2, 2026 21:06
…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.
@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch from 0e2f700 to 6d68d32 Compare September 2, 2026 21:32
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski
wenzowski marked this pull request as ready for review September 2, 2026 21:59
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 6d68d32 into main Sep 2, 2026
19 of 20 checks passed
@wenzowski
wenzowski deleted the claude/suite-subject-retirement-g2g00c branch September 2, 2026 22:19
wenzowski added a commit that referenced this pull request Sep 3, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant