Skip to content

Fix the could-not-look channel, and every punt it was blocking - #808

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

Fix the could-not-look channel, and every punt it was blocking#808
wenzowski merged 8 commits into
mainfrom
claude/cloud-843-bundle-g-yvi8p9

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #796. That PR refused four members and filed two defects; four of those refusals were punts — deferrals I had the means to close — and the two defects I worked around rather than fixed. This closes them.

Closes CLOUD-1049
Closes CLOUD-1276
Closes CLOUD-1218
Closes CLOUD-1150
Closes CLOUD-1206

DO-NOT-CLOSE CLOUD-843 — bundle G is one wave of that campaign, not the whole of it. Members 1, 3, 5, 6 and 7 have landed across #796 and this PR; members 2 and 4 are refuted with measurements, recorded on their own rows.

The PR has one spine. CLOUD-1049 is why three of the four punts were punts: without a working could-not-look channel, every remaining retirement would have to ship a dead arm or a changed: arm apologising for one. Fix the engine first and the retirements become honest ports.


1. CLOUD-1049 + CLOUD-1276 — one guard, two rows

Filed as two defects on two surfaces; they were the same line.

policy_rule built the tree document and then discarded it whenever anything failed to acquire. tree_document had already populated input.tree.missing correctly — nothing read it. So a module's could-not-look clause could not fire, and because the return was unconditional, neither could any other predicate in the same module, including one whose body is true. A gate switched off by the state of one of its own inputs, at exit 0.

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

The method matters for anyone re-measuring this class: an unconditional probe arm is what separates "the channel is empty" from "the module never ran". Two earlier measurements used a probe reading only missing, and so reported the smaller half.

The guard now sits below deny and fires only when the module said nothing. A module carrying the clause speaks; one carrying none still abstains, so CLOUD-251's "never an empty deny set" is intact.

2. .lock reachability — and a dead gate that was already on main

Format::for_path searches Format::extensions() and no variant owns lock, so a declared .lock was UnknownFormat before a byte was read. That was not hypothetical: policy/lock-entry-complete.rego was registered on main, loading clean, passing its own four test_ rules and enforcing nothing on every run, because input.tree.staged["mise.lock"] never resolved and Rego reads undefined as does not hold.

A row may now declare the format its extension cannot name. It is a fallback, consulted only where the extension names none, so it cannot relabel a .json as TOML.

3. CLOUD-1218 — a capped floor measures the volume, so it re-measures

Two candidate fixes were each refuted by a landed test before the discriminator turned up in the suite's own words: "the SPEND and the FLOOR are two different facts once the cap bites".

the cap what the number is how it behaves
did not bite a cost — what a lap of this shape costs ratchets, monotone, as before
bit a capacity — what this volume leaves over re-measured on a leaner close

Measured here: a floor of 14431MB set by a lap that left exactly that, later laps reclaiming to 14336MB, five consecutive refusals and one unnecessary 12 GB cold rebuild, recovered only by deleting a journal no message named. The generation stamp bump un-wedges every clone already stuck.

4. CLOUD-843 member 5 — unlanded-check retired in-process

stop_nudges spawned the program with empty stdin and no arguments, and the program then ran batten state list — while record_state is called in-process two lines above. The read is in-process now, kind:mechanism, and the stub in stop_posture.rs is replaced with real state. A by-hand fixture caught my own first version keying on the hook anchor root rather than git::repo_root: batten state list showed the finding and the hook stayed silent.

5. CLOUD-843 member 7 — lock-complete retired

The largest single unit in the bundle: 419 lines of bash and a 37-case suite become policy/lock-complete.rego, eight predicates over input.tree.staged plus one over the line surface. Unblocked by §2.

policy/lock-entry-complete.rego is subsumed rather than left beside it — two rows over one question is the second authority this repo refuses everywhere else. Its verdict token is raised by the new module, so the registry row is conserved.

The staged/line_sources split is deliberate and is the one place a verdict reads the working tree. Every lockfile and manifest decision comes off the index (CLOUD-227: a cold mise install rewrites mise.lock behind the author's back, so a gate reading the checkout was red in every sandbox and green in CI for the same commit). The workflow clause cannot use stagedgit::staged_facts looks each declared path up in the index by name, so a workflow set could only reach it as a hand-maintained inventory, and a workflow missing from that list is a silent hole in exactly the gate that closes one.

Nine of the thirty-seven cases carry a // changed: or // subsumed: arm with its reason. The load-bearing ones: BATTEN_LOCK_PLATFORMS is gone rather than ported (a module reads no environment, and a knowable string anyone can spend is not config); both exit codes are 2, which is the one exit table with no per-verb exception, with the distinction preserved as V-LOCK-UNREADABLE; and the could-not-look arm is conditioned on a staged mise.toml declaring [tools], so it cannot speak in every fixture repository inheriting this config.

6. CLOUD-1150 §2 and CLOUD-1206 — two predicates rules-drift owed

restated-arm-count-drifts holds a restated CLOSED COUNT over a rule's arms to the modules' own rule heads. It caught its own file on the first run: .claude/rules/toolchain.md said "three shapes on its addition side" and admitted_addition carries four, CLOUD-1224's arm having arrived unmentioned — the same shape as the "one, and only one" claim that a grooming session wrote into two dispatched agent prompts and five issue bodies.

schema-key-undocumented is predicate 3 run backwards: an emittable key the file claiming to enumerate them does not name. Measured 2026-08-30 the schema declared base-delta and symbols and policy-modules.md named neither, under its own sentence promising the lists were held in both directions.

Both are scoped so they cannot invert the discipline they enforce: prose naming a rule without a count is untouched, and a file making no authority claim is silent. One arm was added to named_keys for the subscripted spelling — eight of the engine's keys carry a hyphen and are only ever written input.tree["git-history"], so without it this predicate's first run would have been eight false findings.

Both anchors are line-oriented over prose prettier owns, so a compiled case asserts over the committed files that a reflow has not pushed either across a line break.

The two Weakens: declarations, and why they are not relaxations

config-lint reads two smells against origin/main, and both are syntactic readings of a change that strengthens:

  • rule-removed rule[lock-entry-complete] — the retirement in §5. That row decided nothing on every run; its successor decides nine predicates over the same index.
  • rule-predicate-changed rule[rules-drift].line_sources — a widening. policy/*.rego joins the sources that row already read, which is what lets §6's arm-count predicate see the modules it asserts about.

Neither was groomed as a **Weakens:** clause before the claim, so the commit trailers are the whole of the declaration rather than something backdated into a Ready block after the fact.

What stays refused, with the reason sharpened

tree-clean (member 4) — a policy row has no glob by construction: RuleKind::Policy's column list omits it and policy_rule returns before the glob gate is reached, so there is no scoping mechanism for a tree-scoped policy rule at all. All five self-naming-glob rows are kind = "command". A verdict, not a punt.

suite-bench (member 2) — refused by a landed mechanism rather than by effort. tests/suite-bench-check.bats declares both programs as its subject and SubjectFacts::died is .all(), so the pair is all-or-nothing; the producer is a writer, whose only has_policy_surface is a new verb that would put a benchmark-report generator in a completion gate; and V-PORT-SUBJECT-GOVERNED — landed on main this week — refuses the port arm for a live governed subject by design, its own commit message naming the two suites stranded that way. Recorded on CLOUD-1163 with the route that would unblock it.

Verification

3664 cargo tests, 43 policy bundles / 519 cases, 125 bats suites, batten-check clean, mutate census clean over 113 gates, full verify green (fast-forward-green).

Refs: CLOUD-843, CLOUD-1163, CLOUD-1164, CLOUD-1203, CLOUD-1268, CLOUD-251, CLOUD-1224

@linear-code

linear-code Bot commented Sep 1, 2026

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

Why

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

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

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

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

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

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

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

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

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

Why this is not CLOUD-874

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

Why it matters more than one module

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


Refinement — Ready

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

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

Acceptance

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

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


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

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

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

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

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

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

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

What that costs the retirement campaign, concretely

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

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

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

What §2's acceptance should now read

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

CLOUD-1276 A `line_sources` file the boundary cannot decode reaches neither `lines` nor `missing`, so a module's could-not-look arm is unreachable and the run is a clean exit 0

Why

Measured 2026-09-01 at origin/main a9ff081, over the compiled binary, while landing policy/memories.rego (CLOUD-1163).

A tracked *.md file whose bytes are not UTF-8 is selected by a row's line_sources glob and then simply does not arrive. It reaches neither input.tree.lines nor input.tree.missing. The module's missing clause therefore never fires, and batten check --rule memory-graph returns exit 0 with no finding — a clean pass over a file nobody read.

That is the vacuous pass the channel exists to prevent, stated in schema/policy-input.schema.json on the key itself: "Could-not-look, and NOT a Fact: a declared path the engine could not acquire. Distinct from an empty result, which is the distinction that keeps a vacuous pass out (CLOUD-251, CLOUD-845)."

Reproduction

# a fixture repo with one rule whose line_sources glob includes "*.md"
printf 'mem:\xff\xfe' > BROKEN.md
git add -A && git commit -m x
batten check --rule <row>     # exit 0, no finding, BROKEN.md named nowhere

The case is landed as crates/batten/tests/memories.rs::an_unreadable_referrer_does_not_reach_the_could_not_look_channel_today, written to assert the MEASURED behaviour with a comment saying to delete it and assert the finding once this row lands.

Why this is not CLOUD-1049 again

CLOUD-1049 is Done and closed the parsed-document half: "An unparseable declared source reaches no policy module and reports no cause, so input.tree.missing is empty where a module's own test proves it populated." A parse failure on a declared sources path populates the channel now.

**This is the LINE half, and it is a different acquisition path. **Rule::line_sources (CLOUD-864) selects by glob and hands the bundle raw lines; Format::for_path answers None for markdown, so there is no parser to fail and nothing on that path treats an undecodable read as a cause. The file is neither parsed-and-refused nor read — it falls out between the two.

.claude/rules/policy-modules.md predicted exactly this reader and warns against inheriting CLOUD-1049's closure as coverage: "This used to add that the engine half did not populate for a parse failure, so the clause was right and the channel empty. CLOUD-1049 shipped on 2026-08-25 and that parenthetical is stale — do not read it as licence to leave the clause untested. Its own acceptance requires the cause to be distinguishable from Absent, and to prove it in the second tier over the compiled binary rather than with with input as, which is the only way to tell a populated channel from one nothing fills."

That is what was done, and the channel is empty for this cause.

What it costs, and why it is worth a row rather than a note

Every module reading line_sources has an unreachable could-not-look arm. That is shell-retirement, suite-subject-retirable, ci-parity, bats-invocation, remedy-authorship, memories and the rest of the line-reading set — each writes the clause the rules file demands, each is green, and none of them can fire it through this cause. A gate that reports clean over a file it never opened is indistinguishable from a gate that read it and found nothing, which is the one distinction the whole missing design turns on.

The exposure is new to the retirement campaign rather than inherited. The shell predecessors read their sources with grep/sed over the tracked set, and those are loud on undecodable bytes. memories-check.sh:102 piped git ls-files into xargs grep; a binary file there is noisy, not silent. So every line-reading port trades a loud failure for a quiet one, and nothing reports the trade.

Deliberately not in scope

Widening what lines carries. Changing any predicate. Whether a non-UTF-8 file should be judged — it should not; it should be named, which is the whole difference.


Refinement — Ready (populate missing on the line-acquisition path)

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

  • **Authority boundary (§1). **crates/batten/src/rules.rs's line acquisition, crates/batten/src/facts.rs if the cause needs a spelling, crates/batten/tests/.
  • Computable predicate (§2). A path a row's line_sources glob SELECTS and the boundary cannot decode appears in input.tree.missing with its cause, exactly as a declared sources path that will not parse does since CLOUD-1049.
  • Deliberately not in scope (§2). The documents/sources path, which already does this. A path no glob selects — that is a stated skip, not a miss.
  • **Effect (§3). **read.
  • Output and exit (§5). Pointer-only: the path and the cause, never a byte of the file — which matters more here than usual, since the file is undecodable and its bytes are exactly what must not travel.
  • **Commit / bump (§6). **fix(facts) — patch.
  • Test obligation (§7). Over the compiled binary, never with input as, for the reason this row exists. Shown able to fail per CLOUD-418: the mirror is a decodable file in the same glob reaching lines normally, so the case cannot pass by reporting everything as missing. Delete memories.rs's measured-behaviour case and assert the finding in its place — that file names this row and says so.
  • Blockers (§8). None. relatedTo CLOUD-1049 (the document half, Done), CLOUD-845 (the dead-gate class), CLOUD-251, CLOUD-864 (the column), CLOUD-1163 (found here).

Acceptance

  • A non-UTF-8 file selected by a line_sources glob is named in input.tree.missing with a cause.
  • A module's could-not-look arm fires on it, asserted over the compiled binary.
  • memories.rs's measured-behaviour case is replaced by the finding assertion.
  • Absence and unreadability remain distinguishable — a path no glob selects is still a stated skip and not a miss.

Found while landing CLOUD-1163's memories-check retirement, by writing the second tier the rules file asks for and discovering the channel it reads is empty.

CLOUD-251 `graph-check` cannot tell "not Ready" from "not judgeable": a frontier omission is unattributable, and a set with no relations still reports the board coherent

Why

graph-check is both the board gate and the scheduler: its stdout is the ready frontier, and its own header states the property that replaces a dispatcher — "Every agent computing this independently gets the same answer; that shared determinism is what replaces a dispatcher." That property does not hold, because the frontier is silently relative to whatever stdin happened to carry, and a frontier omission is not attributable.

mise-tasks/graph-check:92 is the whole defect:

jq -c --arg id "$id" '.[] | select(.id == $id)' <<<"$issues" | "$lint" >/dev/null 2>&1 || continue

ready-lint already distinguishes the two cases that matter — exit 1 is a violation printed <id>:<line> <rule>, exit 2 is "stdin is not a get_issue payload with a .description field" — and || continue throws both away. Measured over the same 25-payload Phase 1 closure, one run with CLOUD-30's description key deleted and one with its §6 arrow corrupted to featminor produce byte-identical output:

wip 0
frontier CLOUD-7
frontier CLOUD-15
frontier CLOUD-63
frontier CLOUD-64
graph-check: board coherent (25 issues)

Exit 0 both times. "This issue is not Ready" and "you did not pipe me enough to judge whether it is Ready" are the same absence: one missing frontier line and nothing else. An agent reading that frontier pulls the wrong work, or leaves ready work unpulled, with nothing to notice.

The same shape reaches the coherence verdict. Input validation at :40 requires only has("id") and has("status"), so a payload set carrying no relations key at all is accepted and reported graph-check: board coherent — measured on a two-issue set with no relations: exit 0, that message, no frontier. The acyclicity and non-dangling claims are vacuous over a set whose edges the caller projected away, and the wording asserts them anyway. :110 prints the piped count, which is the only present hedge; a count says how many payloads arrived, never which fields they carried.

This is the repo's own no-silent-caps rule turned on the gate that schedules the work, and the sibling of the discipline CLOUD-234 specifies for status claims — "an id outside the piped set is reported as unjudgeable, never guessed" — applied to the frontier's own ready-lint conjunct. It is measured, not hypothetical: the Phase 1 milestone run that found it piped exactly such a projection and was told the board was coherent.

Acceptance

  • A Todo issue excluded from the frontier is attributable: the reason is reported, and "its Ready block fails" is a different report from "its payload could not be judged".
  • A payload that cannot be judged is never silently equivalent to a payload that was judged and failed. ready-lint's exit 2 is surfaced, not swallowed.
  • A coherence verdict is not asserted over a property the piped set cannot support: a set carrying no blockedBy data anywhere does not read as an acyclic, non-dangling board.
  • The gate still exits 0 on a genuinely coherent set whose Todo issues genuinely fail their Ready blocks — an unjudgeable payload is the caller's error, a failing block is not.

Refinement — Ready (the frontier's own exclusions become attributable; ready-lint's existing exit codes are the whole mechanism)

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

  • Source of truth (§1). One artifact: mise-tasks/graph-check. No new input, no new fetch, and above all no second copy of the Ready predicate — the distinction this issue surfaces already exists in mise-tasks/ready-lint's exit contract (0 satisfies, 1 violation, 2 unreadable payload), which its own header declares, and the change is that graph-check reads that status instead of discarding it at :92. The two rule-id vocabularies stay separate: a Ready-clause violation keeps ready-lint's rule ids and is never re-derived here, while the new reports are graph-check's own, alongside in-progress-unassigned, in-review-no-pr, blockedby-cycle and dangling-blocker.
  • Computable predicate (§2). mise run test:bats over tests/graph-check.bats, reached through the shared hk gate and so through mise run ci — the suite that already drives this script from synthetic payload sets (16 cases today, grep -c '^@test' tests/graph-check.bats being the authority rather than a number restated later). No new task and no new gate step. Deliberately not a batten.toml rule under batten check: the predicate is a join over a set of tracker payloads arriving on stdin, and every reachable rule kind evaluates a file tree — the same capability gap CLOUD-95 already registers for an out-of-tree artifact as a check input, so the engine grows there and not in this script. The anti-vacuity conjunct is the load-bearing one, because this issue exists because a check reported success over an input it could not judge: the suite must contain a case where a payload's unjudgeability is reported and a case where a real Ready failure is reported, and they must not be satisfiable by the same expected bytes.
  • Output & exit (§5). Pointer-only, matching every other line this script emits: an issue id and a rule id on stderr, never a description body — issue bodies can carry consumer detail, and non-negotiable rule 4 holds here as everywhere. An unjudgeable payload is a caller error over the same axis as :40's existing refusal, so it reports and exits 2 ("stdin is not a set of get_issue payloads" is already that code); a Todo issue whose Ready block genuinely fails stays a normal frontier exclusion and does not move the exit code, since a board full of unrefined issues is not an incoherent board. The 1 reserved for a falsely-signalling board is unchanged. The frontier lines keep their current bytes so no caller parsing them breaks; what changes is that an exclusion is accompanied by its reason and that the closing verdict does not claim a property the input could not carry.
  • Commit / bump (§6). cino bump — a gate script plus its bats suite, no crate source, so release-plz releases nothing.
  • Test obligation (§7). tests/graph-check.bats, synthetic payload sets as the suite already uses: (a) the measured pair — one set with a Todo issue's description key absent and one with the same issue's Ready block genuinely failing — no longer produce identical output, which is the regression that fails today; (b) the unjudgeable case reports its issue id and exits 2; (c) the genuinely-failing case reports its exclusion and leaves the exit code where a coherent board puts it; (d) a set carrying no blockedBy data anywhere does not emit the unqualified coherence claim; (e) a fully-populated coherent set emits today's bytes unchanged, so the frontier's existing contract is pinned rather than re-specified; (f) anti-vacuity — a set in which every Todo issue is judgeable and passes still exits 0 with no new report, so the rule cannot decay into one that always fires.
  • Blockers (§8). None. ready-lint's exit contract, the payload-set input shape, and the bats harness are all in the tree. relatedTo CLOUD-234, whose "unjudgeable, never guessed" acceptance is the same discipline for a different claim in this same script and which should land in one pass with this if both are pulled together, CLOUD-175 (the issue that made the board discipline computable and shipped this script), and CLOUD-95 (the §2 capability gap).

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

Why

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

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

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

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

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

The classification, by what each task invokes

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

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

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

The pilot

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

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

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

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

Sequencing, and the one trap

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

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


Refinement — Ready

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

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

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

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

Acceptance

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

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


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

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

What the run showed

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

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

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

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

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

And wave 1 is smaller than the bucket count suggests

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

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

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


BUNDLE W0 — the unblocker. Dispatch-ready now.

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

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

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

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

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

Dispatch prompt — one paste, self-contained

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

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

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

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

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

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

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

Wave 1, after W0 lands

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

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


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

Gates per PR, not agents

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

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

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

Two tracks, run concurrently

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

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

The bundles

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

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

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

Order

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

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

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

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

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

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

Dispatch is BY HAND, and that is settled

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

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

Why

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

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

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

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

Measured cost, in this repository, this week

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

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

The mechanism half, and its coupling

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

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

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

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

The prose half is landing separately and immediately

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


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

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

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

Acceptance

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

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

CLOUD-1206 `rules-drift` claims it holds `.claude/rules/policy-modules.md`'s key lists to the generated schemas in both directions, and two tree keys the engine emits — `symbols` and `base-delta` — are absent from the file

Why

.claude/rules/policy-modules.md enumerates the input.tree.* keys a tree-scoped module may read, and closes that section with an explicit guarantee:

schema/policy-input.schema.json and schema/policy-call.schema.json are the authority and are generated — do not hand-edit either, and do not restate the key set anywhere else. rules-drift holds the lists above to those two files, so a key named here that the engine cannot emit is a finding rather than a trap for the next author.

Measured 2026-08-30, the other direction is unheld. schema/policy-input.schema.json declares eleven tree keys; the rules file names nine of them. Two are absent:

key policy-input.schema.json named in policy-modules.md
base-delta :10 no
symbols :246 no

base-delta is not obscure — CLOUD-1162 is scoped around it ("input.tree["base-delta"] carries added/edited/deleted/code-changed"), and filed-over-own-diff already decides over it. An author reading the rules file to learn what is available will not find it, and will either re-derive a diff another authority already owns or conclude the predicate is inexpressible and file a fact-family row for something that ships.

Why this is the same defect class the file is about, one level up

policy-modules.md exists to warn that a key from the wrong surface is a silent dead gate — Rego reads undefined as undefined, the body never holds, the violation set is empty, and a dead gate and a clean tree are byte-identical. This is the mirror: a key that does exist, absent from the list an author is told is authoritative, produces the opposite error — a fact family filed for a fact already built. Both fail silently and both are found by reading the schema rather than the prose.

The file's own §"What this file does not gate" already concedes "rules-drift holds the key lists above to the generated schemas, and nothing here holds anything else… This buys currency, not compliance." Currency is exactly what is not bought, and that is what this row corrects.

Adjacent, and not the same row

CLOUD-1150 is the same shape over .claude/rules/toolchain.md's rule-NAME set: a restated set rules-drift cannot see. This row is the key set in policy-modules.md, where the file makes a stronger claim — it says the check runs in both directions — so the gap is a false assurance rather than an unclaimed one. If the fix is one mechanism serving both, that is a merge to make deliberately, not by assuming.

A THIRD surface, measured 2026-08-31, and it drifts against the MANIFEST rather than a schema

Recorded here rather than filed, because this row and CLOUD-1150 are already two instances of one class and a third row would be the restatement CLOUD-1166 refuses. It widens the class rather than repeating it: the authority is neither generated schema.

.claude/rules/rust.md's concurrency section states, as a fact about the tree:

"there is no async runtime in the crate today: tokio appears nowhere in Cargo.lock, and there is no async fn and no .await. tests/ambient_authority.rs is the gate on that rather than this paragraph."

tokio **is a DIRECT dependency. **crates/batten/Cargo.toml:90-96 carries hyper, hyper-rustls, hyper-util and tokio, landed by CLOUD-745 when curl was retired.

**The sharp part is that the OTHER authority already knows. **clippy.toml:22-31, on the same subject, reads "They were written while tokio *was in no dependency table … they were to go live the day an HTTP client arrived. **It arrived. ***tokio IS in the shipped closure now", and :56-63 explains what holds the line instead (the feature list is rt, net, time, so rt-multi-thread is a compile error rather than a lint). So this is not "nobody noticed" — one authority was updated and the rules file was not, which is precisely the currency gap this row's title is about.

Why it is a third surface and not a duplicate of the two above:

row prose file held against
this row policy-modules.md key lists schema/policy-input.schema.json (generated)
CLOUD-1150 toolchain.md rule-NAME set the module's own rule names
new rust.md dependency claim Cargo.toml / Cargo.lock — a manifest, not a schema

A set-equality gate over two generated schemas does not reach a prose sentence asserting a crate is absent. Whether that is in scope for one mechanism or is a separate predicate is this row's call to make deliberately, exactly as it already says of a CLOUD-1150 merge — it is not assumed here.

The immediate cost is already contained: CLOUD-1260 and CLOUD-1264 both now carry an explicit "rust.md is stale on this point, read the manifest and clippy.toml" caution, because an implementer sent to build an MCP client is exactly who that sentence misleads. The file itself is still wrong.

Refinement — Ready (hold the doc to the schema in the direction that is unheld)

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

  • Authority boundary (§1). .claude/rules/policy-modules.md (the two missing keys, with the one-line description each other key carries), and whichever mechanism rules-drift is — its batten.toml row and/or crates/batten/tests/. No mise-tasks/ program and no tests/**/*.bats is edited or added. schema/policy-input.schema.json is generated and is not touched.
  • Computable predicate (§2). The set of tree keys named in .claude/rules/policy-modules.md equals the set schema/policy-input.schema.json declares — as a set equality, both directions, not a subset check. Same for the mediated_call list against schema/policy-call.schema.json.
  • The direction that is missing is the one to build (§2). A key in the doc that the schema lacks is already a finding. A key in the schema that the doc lacks is the untested half, and it is the half that was live: two keys, one of them load-bearing for a Todo row.
  • Deliberately not in scope (§2). Changing what any fact carries. Adding a key. Whether symbols and base-delta should be readable by a module — they are, and this row records that they are documented, not that they are correct. Merging with CLOUD-1150's mechanism, which is that row's call.
  • Effect (§3). read — the gate reads two files and compares two sets.
  • Output and exit (§5). Pointer-only: the key name and which side is missing it, never a span of either file. Exit follows the 0/1/2/3 table; an unreadable or unparseable schema is 3, never a false pass — a set-equality gate that cannot read one side must not report equal.
  • Commit / bump (§6). fix(policy) — patch. The doc edit alone would be docs; the mechanism half is what carries the type.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/. Shown able to fail per CLOUD-418, and the four cases are forced by the predicate: a key in the schema and not the doc is reported (this is the case that would have caught the live defect); a key in the doc and not the schema is reported; an unreadable schema exits 3 rather than passing; and the anti-vacuity control — the corrected tree passes.
  • Blockers (§8). None. relatedTo CLOUD-1150 (the same class over toolchain.md's rule-name set), CLOUD-845 (the dead-gate class this mirrors), CLOUD-876 (the general schema mechanism).

Acceptance

  • symbols and base-delta are named in .claude/rules/policy-modules.md's tree-key list, each with the same one-line treatment the neighbouring keys get.
  • rules-drift fails on a tree where a schema key is missing from the doc — demonstrated by removing one and observing the refusal, not asserted.
  • The set equality holds for the mediated_call list too, checked in the same pass.
  • The file's own §"What this file does not gate" is corrected if its currency claim is now stronger than it was.

Found while pressure-testing the fact-family rows against the imperative surface: a row was nearly filed for a projection base-delta already carries.

CLOUD-1218 The lap journal records a hand-emptied `target` as a WARM lap, so the ratchet inverts and the floor climbs to one nothing can satisfy

Why

CLOUD-1157's lap journal ratchets each floor to the worst consumption it has observed, which is right. What decides which floor a lap is charged to then mislabels a from-scratch rebuild as warm, so the warm floor learns a cold lap's demand.

CORRECTED 2026-08-30, third instance (PR #770). This paragraph originally read "what decides which floor a lap is charged to is whether the escalation dropped a basis-moving root." **That has not been true since **CLOUD-1157 (#756) landedbasis_of already derives the basis from the TREE, not from the escalation:

// crates/batten/src/prune.rs:1596
fn basis_of(root: &Path) -> Basis {
    let populated = directories_named(root, "deps")
        .iter()
        .any(|deps| std::fs::read_dir(deps).is_ok_and(|mut e| e.next().is_some()));
    if populated { Basis::Warm } else { Basis::Cold }
}

The residual defect is the .any(), and the function's own doc comment already names it (prune.rs:1582-1589): *"this reads EVERY *deps *under the root, so a populated *target/release/deps reports warm while the DEBUG build the lap is about to run is cold." One populated profile masks another being empty.

This matters because §2(b) below is written against the old mechanism and prescribes a fix the code already has. Reading it as written sends an author to replace an escalation check that is no longer there.

Measured on this container, 2026-08-30, while landing CLOUD-746. $GIT_DIR/batten-prune/laps.json held:

{"open":{"free_mb":9151,"basis":"warm","head":"d3788d66","measured":"2026-08-30"},
 "ratchet":{"warm":{"mb":22861,"head":"5647a306","measured":"2026-08-30"},
            "cold":{"mb":4640,"head":"090dd1f5","measured":"2026-08-30"}}}

cold 4640 MB below warm 22861 MB is impossible — a cold lap consumes more than a warm one by definition, and [prune]'s declared numbers say so (warm 6242, cold 14914). The inversion is the tell, and it is the cheapest possible detector.

How it got there, and why an agent will keep doing it. A land lap that refuses on disk prints "Free space outside ./target, or start a fresh session." The obvious reading is rm -rf target, which is outside target-prune entirely. Each such lap then rebuilds from nothing and is charged to warm, ratcheting the warm floor toward the cold one. After a handful of laps the warm floor stood at 22861 MB — above anything a completed lap on this box can leave free — so every subsequent lap refused, in both directions: warm target failed the opening reading, cleared target failed the closing one.

The recovery is to delete the journal, which restores the declared floors. That is not discoverable from the refusal: the message names free space, the floor, and the roots it could not reclaim, and never that the floor it is quoting is one the tool taught itself.

SECOND INSTANCE (2026-08-30, landing PR #751) — and it defeats this row's own detector

Reproduced on a different container, different branch, same mechanism. Journal at refusal:

{"open":{"free_mb":8538,"basis":"warm","head":"6e0e7f6c","measured":"2026-08-30"},
 "ratchet":{"warm":{"mb":15798,"head":"c96288c1","measured":"2026-08-30"},
            "cold":{"mb":21519,"head":"c96288c1","measured":"2026-08-30"}}}

Against declared warm 6242 / cold 14914, both floors are self-taught and both are ~2.5x and ~1.4x the declared value. Two hand-emptyings produced them, in this order:

act journal's reading
rm -rf target/debug/incremental (the one root target-prune says it cannot reclaim) "consumed 15798MB — worse than any warm lap on record, so the observed warm floor rises to 15798MB"
rm -rf target (after the above still refused) "consumed 21519MB — worse than any cold lap on record, so the observed cold floor rises to 21519MB"

After that, every lap refused in both directions exactly as this row describes, and land failed three consecutive times.

Why §2(a) and acceptance bullet 1 would NOT have caught it

This ratchet is correctly ordered — cold 21519 > warm 15798 — so "a ratchet whose cold observation is below its warm one is refused" is silent here. The inversion is one symptom of poisoning, not the class. The recorded instance inverted because its two hand-emptyings happened to charge the wrong buckets; this one charged the right buckets and simply ratcheted both past what the box can satisfy. A detector keyed on ordering therefore catches the first instance and misses the second, which is the more ordinary shape — an agent following the refusal's own advice twice, in the order the refusal suggests it.

So the predicate wants a second conjunct, and §2(b) is the one that generalises: derive the basis from what the tree WAS at lap open. Under (b) both of my laps are cold and neither teaches the warm floor anything, which is the correct outcome and is reached without reference to ordering. Worth stating on the row because (a) is the cheaper check and reads as sufficient — it is not, and a fix that ships only (a) leaves this instance live.

A cheap third guard, orthogonal to both: refuse to ratchet a floor above the declared one by more than some factor without saying so. A learned number 2.5x its own config is more likely a mismeasurement than a real budget, and the refusal that quotes it should say which it is — which §5 already asks for and this instance makes concrete.

The recovery worked, and its cost is the real damage

rm -rf "$(git rev-parse --git-dir)/batten-prune" restored declared floors and admitted the next lap at 8130MB free against warm 6242MB. The cost was one full cold rebuild — the rm -rf target this row predicts an agent will reach for, which consumed 21519MB and ~20 minutes of wall clock, and which was never necessary: the journal was the whole problem and deleting it alone would have sufficed at the very first refusal. That is the measured price of the undiscoverable recovery, and it is the strongest argument for §5's "observed vs declared" wording landing with the fix rather than after it.

Why it is not CLOUD-861's or CLOUD-1030's. CLOUD-861 is the once-per-lap precondition, and the closing reading it added is what reports this — correctly, over a poisoned number. CLOUD-1030 is the escalation invalidating the basis that certified the lap. This is the third: an external reclaim mislabels the basis a lap is recorded under, so the ratchet learns the wrong thing and the error compounds across laps rather than affecting one.

Refinement — Ready

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

  • Authority boundary (§1). crates/batten/src/prune.rsLapJournal, OpenLap.basis and Ratchet. [prune]'s declared floors are untouched: this row makes the recorded basis honest, it does not move a measured number. No new config key and no runner decides any part of it.

  • Computable predicate (§2), REWRITTEN 2026-08-31 after both original clauses were implemented and refuted. One clause, decidable over the compiled binary: a profile cargo has BUILT whose deps **is missing or empty makes the tree cold. **target/<profile>/.fingerprint is the marker — cargo writes one per profile it has built and leaves it behind when deps goes, so the absence becomes visible. Every .fingerprint's sibling deps must be populated; no .fingerprint anywhere falls back to the pre-existing reading, which is what keeps this a narrowing rather than a new requirement. It needs no guess about which profile the caller will build next, so the comment's refusal of profile scoping still stands.

    (a) A ratchet whose ~~cold~~ observation is below its ~~warm~~ one is refused. REFUTED. Silent on two of three instances (one journal carried cold: null; one carried a correctly ORDERED pair). The strengthened form — refuse a warm observation at or above the declared cold floor — was implemented and turned a_warm_laps_consumption_does_not_raise_the_cold_floor red: that case deliberately drives a warm lap consuming 22000MB against a 14000MB cold declaration, because a warm observation under it cannot discriminate a per-basis ratchet from a shared one, and its comment records that as a surviving mutation. The premise is also a unit error — the declared floor is a free-space budget, not a ceiling on consumption — and no factor separates the real case (3.96x) from the fixture's legitimate one (3.67x).

    (b) …narrowing ~~basis_of~~'s ~~.any()~~ to ~~.all()~~. **REFUTED. **directories_named only yields directories that EXIST, and the reclaim an agent actually performs REMOVES target/debug/deps, which drops that profile out of the walk and leaves a populated target/release/deps satisfying either quantifier. .all() only helps for a deps that survives but is empty, which is not what any of the three instances did.

    "Empty or near-empty root" was also wrong and would miss all three. Mine left target/release, target/debug/build and ~660 MB standing; the second instance deleted only target/debug/incremental. deps is the build basis; the rest of the tree is not.

  • Effect (§3). Unchanged. target-prune keeps its classification; what changes is which bucket an observation lands in.

  • Output & exit (§5). Unchanged, except that the refusal should be able to say the floor it quotes is observed-and-inverted rather than declared — the current message gives a reader no way to tell a learned floor from a configured one. Pointer-only throughout: megabytes, a head and a date, as today.

  • Test obligation (§7). Over the compiled binary, shown able to fail per CLOUD-418. The discriminating case needs TWO profiles: a_tree_emptied_by_something_other_than_the_escalation_is_still_a_cold_one deletes deps on a one-profile tree, so the walk comes back empty and any quantifier answers cold — it cannot see this class. Red before: a built profile whose deps is REMOVED while another profile stays intact. Green and staying green: every built profile intact is still warm, and a tree cargo never fingerprinted still takes the fallback — without that pair the fix is satisfied by charging everything to cold, which raises the COLD floor instead and fails the same way one bucket over.

  • Blockers (§8). None. relatedTo CLOUD-1157 (whose journal this is), CLOUD-861 (the closing reading that surfaces it), CLOUD-1030 (the escalation-invalidates-basis half) and CLOUD-1153 (the other way this refusal misreports its own cause).

Acceptance

  • ✅ A profile cargo has built whose deps is missing or empty makes the tree cold, whichever other profiles survive. Landed on refactor(ci): retire derived-check, man-pages and ci-local-parity; repair the lap journal's basis #770 (856c3746).
  • ✅ A tree whose every built profile is intact is still warm, and a tree cargo never fingerprinted is judged by the pre-existing reading. Landed on refactor(ci): retire derived-check, man-pages and ci-local-parity; repair the lap journal's basis #770.
  • ✅ The refusal names the journal that holds a learned floor, so the recovery is discoverable from the message rather than costing a full cold rebuild to find. Landed on refactor(ci): retire derived-check, man-pages and ci-local-parity; repair the lap journal's basis #770 (1c896f89).
  • ~~A ratchet with ~~~~cold~~ ~~below ~~~~warm~~ ~~never decides a refusal. ~~Withdrawn — see §2. Silent on two of three instances, and the strengthened form conflicts with a landed measured test.
  • ~~A correctly-ordered but inflated ratchet is also caught. ~~Withdrawn with it: no threshold separates the real case from the fixture's legitimate one. With the basis now honest, a cold lap is charged to the cold ratchet and the warm one is never taught a rebuild's demand, which is the route §2 takes instead.
  • Still open, and the reason this row is not Done: the ratchet remains unbounded, so a genuinely mismeasured observation of any basis is still learned permanently. That needs a predicate over satisfiability — a floor above what a completed lap can leave free can only ever refuse — rather than over ordering or magnitude. Not attempted here.

Found while landing CLOUD-746: six consecutive land laps refused on a floor the tool had taught itself from my own rm -rf target, and the fix was deleting a file no message named.

CLOUD-1164 Retire the 66 remaining single-program units — 251.3s, one delta each, with the blocked/unblocked verdict recorded per program

UNSCHEDULABLE AS FILED — 66 units in one row, never claimed, and it is why six bundles were built around it (2026-08-31)

The blocked-column work below is now largely correct. The row's SHAPE is what is left, and**
it is not a content problem. Nobody claims a 66-unit row. Over one session four bundles
(A–D) were dispatched and two more were sized, and every one of them routed around this

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

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

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

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

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

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

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

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

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

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

hook-latency-drift now has a row

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


Why

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

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

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

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

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

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

Unblocked — predicate expressible over committed tree content today

These need only a successor written. ~30 programs.

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

Two verdicts worth stating because a naive scan says otherwise:

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

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

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

class in the table below its precondition status
forge/board (16 programs) CLOUD-1154 DONEinput.tree.forge exists
out-of-root (4 programs) CLOUD-1167 DONEinput.tree.external exists
third-party exec (10 programs) CLOUD-1171 CLEARS THE WRAPPERS, NOT ALL TENinput.tree["tool-verdict"] exists; see the residue note
transcript content (2 programs) CLOUD-1172 DOES NOT CLEAR THIS CLASS — it landed a COUNT; CLOUD-1254 owns the remainder
engine state (1 program) CLOUD-1203 DONEinput.tree.state exists
stranded (2 programs) CLOUD-1156 DONE
clock / live process — membership is CLOUD-1170's table, not this row's CLOUD-1170 STILL BLOCKED — decided, not built; there is no instant key on the surface

This row's own blockedBy (CLOUD-1154, CLOUD-1156) are therefore both satisfied. What is stale is the prose, and a**
**session reading it will decline ~30 programs it could take today.

One class is settled outright, because the schema names the program

unlanded-check is not merely unblocked. input.tree.state's own description reads:
"the finding pointer lines the engine's own store accumulated on it … these are the***
**lines unlanded-check already reads, so a successor reads what the program read
**rather than a re-derivation that could disagree." *CLOUD-1203 built the fact for this
program by name. Take it first.

Two classes need PER-PROGRAM reading before the verdict flips, not a class-level flip

The precondition landing is not the same as the program being expressible, and this
row's sibling CLOUD-1163 got three verdicts wrong in exactly that gap — by
classifying a program instead of reading it. So:

  • **out-of-root splits. **mcp-attach-check:135 names a DECLARED path under ${HOME}
    and is expressible via external today. transcript-corpus-check:97 reads
    $HOME/.claude/projects, a DIRECTORY whose members are discovered at runtime —
    that is CLOUD-1251's class and is still blocked, because [[rule.external]]
    declares one path per row. Read serena-mcp and doctor-check before assigning
    either.

  • transcript content DOES NOT FLIP, and that is now settled rather than suspected. CLOUD-1172 landed input.facts.extracted"a declared extractor's COUNT over this session's transcript — an integer over typed events, never a byte of the session". Read at origin/main 0683ce53, finding-sink-check is a per-turn join with a negated arm: prose matching a path:line citation, joined against the same turn's tool_use records, each classed against the issue-read-check receipt's fifth field. A count is not grouped by turn and carries no tool name, so it cannot express matched A and not B within the same group. board-payloads:157 extracts get_issue payloads, which is further still from a count.

    The recorded reason was "input.call.transcript is the path only" and that is wrong now — the reason is that the fact is a COUNT by design, and widening it is what rule 4 exists to refuse. CLOUD-1254 owns the remainder and decides per program: a declared extraction whose RESULT is decided over, or an exemption by name. Both programs stay blocked until it answers. CLOUD-1172's own §2 scoped these two retirements out in as many words, so its Done status was never a clearance for them.

The third-party-exec class, read per program — and a CORRECTION to the first attempt at this section

An earlier revision of this section, written 2026-08-31, named four programs as the class's "residue" and was wrong about two of them. It was written by classifying from a bucket rather than opening the files, which is the defect this row states two sections down — committed inside the correction for it. Each verdict below now quotes the program's own #MISE description or a path:line, so the next reader can check it without re-deriving.

input.tree["tool-verdict"] answers a wrapper around a tool whose verdict is keyed to (tool, pinned version, input digest): renovate-config-validator, pkl-check, hook-profile-check, macos-link-check, evaluator-io-check, mutant.

evaluator-closure-check is the class's BEST fit, not its residue. The earlier revision said it "wants a resolved dependency graph, not a tool's pass/fail — a verdict record cannot carry one." Read at origin/main 0683ce53:

  • :98cargo metadata --locked --format-version 1. A third-party tool over Cargo.lock, which is exactly the (tool, pinned version, input digest) key tool-verdict is built on.
  • :83–:89BATTEN_EVALUATOR_METADATA already substitutes a file for cargo's output. The seam a recorded verdict needs is landed in the program.

So "a verdict record cannot carry one" asserts an impossibility that does not hold. The open question is narrower and real: whether the producer records the reachability FINDING (finding name -> pointer, which is tool-verdict's declared shape) or the graph itself — the decision at :104 is currently made in python over the whole metadata. Answer that; do not treat it as blocked.

perf-assert is not a producer and does not belong in this class at all. The earlier revision filed it under "producers, not gates". Its own header: "Gate: every measured invocation path is inside its latency budget, and README publishes the budget this gate enforces" and *"A PURE FUNCTION OF STDIN, the *graph-check/claim-check interface: agents fetch, gates decide." It carries a #MUTANT row at :54, exits 0 pass / 1 over budget or README disagreement / 2 could-not-look, and its §5 clause is "never the raw hyperfine output, never a command line". That is a board-gate-shaped predicate over a produced record plus README's budget column — input.tree.produced/records and input.tree.lines, all landed. Read it and place it; it is plausibly unblocked today.

perf-record and perf ARE producers, and this is why the other two slipped. Three of four in one bucket read as a pattern and the reading stopped. perf-record: "Append a measurement to the trunk's invocation-cost series in git notes … refuses to run off main". perf: *"Measure batten's invocation cost … THIS MEASURES; IT DOES NOT DECIDE. The verdict is *mise run perf-assert's." A producer has no verdict to key on; what it needs is a home for what it writes, which is a different question from expressibility. perf also has no suite (one ledger arm, per the four-programs note below).

Read each of the ten before assigning it, and quote what you read. The rule below is not new — this section is its fifth violation and the first where the violator was the correction itself.

CORRECTION (2026-08-31) — signing-posture was never in the clock class, and this row put it there

The blocked table listed signing-posture 2.1 under clock / live process. It decides over neither. Its own header: "Gate (and, with --repair, the write): no commit is signed by a key that cannot be verified or reproduced (CLOUD-669)" — a key-verification predicate. It is not in CLOUD-1170's member table, and CLOUD-1170 is the owner of that class.

So a program with no blocker here has been parked on CLOUD-1170 and is not in the ~30 unblocked list either — it fell between the two. Its real disposition is unrecorded: read it and place it, rather than moving it to "unblocked" on the strength of this correction alone, which would be the same mistake in the other direction.

The cause is restatement, and it propagated. CLOUD-1170 enumerates the class per program with a stated reason. This row restated three names and added signing-posture; CLOUD-1155 restated six and added land — correctly, since CLOUD-1170 had omitted it; CLOUD-1151 then merged this row's error with that table and published ten names, one wrong and one missing. Three copies, three mutations, the owner never consulted. That is CLOUD-1166's defect one type over — a membership LIST rather than a count — and the fix on every consumer row is the same: cite the owner, never restate it.

A text scan does not settle it either. git grep for date +%s or kill -0 reads false in both directions here: target-ensure delegates its lock to with-lock, hook-latency-drift times through hk, and step-receipt/graph-check merely WRITE a stamp. Membership is what the program decides over, which is the reading rule this row already states two sections down — and which the session writing this correction violated before it followed it (CLOUD-844's defect).

The rule the three wrong verdicts on CLOUD-1163 bought

Read the program before recording why it cannot move. Every wrong blocked verdict
found today came from taking the program's category as its predicate. The reading took
minutes in each case, and in each case the recorded reason would have sent an
implementer to build the wrong thing.

Blocked — REWRITTEN 2026-08-31 against the landed fact surface

The previous version of this table was written 2026-08-30 and listed six blocked classes.
Five of the six had their precondition land and the table was never edited, so it went
on declining ~30 programs a session could take. This is that table with each class's
current verdict, and the row's own blockedBy on CLOUD-1154 and CLOUD-1156
has been removed, both being satisfied.

class verdict today what remains
forge / board (16 programs: bot-issue, linear-check, land-divergence, ready-cites-check, done-check, sonar-gate, branch-age-check, timeout-drift, release-due, ci-drift, attestation-check, release-backfill, nonverdict-assert, land-divergence-assert, land-lock-check, ci-lease-precondition) UNBLOCKED CLOUD-1154 Doneinput.tree.forge is on the surface. Each still owes a per-program read before its verdict is written
out-of-root SPLIT a DECLARED path under a named root is input.tree.external (CLOUD-1167, Done) — mcp-attach-check:135 qualifies. A DISCOVERED SET does not: transcript-corpus-check:97 ($HOME/.claude/projects) and mcp-allow-check:327 (/tmp/mcp-config-cse_*.json) are CLOUD-1251, genuinely open
clock / live process NOT BLOCKED — UNBUILT CLOUD-1170 is DECIDED in writing, its only blockedBy (CLOUD-1177) is Done, and it is Urgent/Todo/unassigned. Blocked means nobody can start; this means somebody must. Read the membership there — never restate it
third-party exec THE FACT EXISTS; THE PRODUCER DOES NOT input.tree["tool-verdict"] landed (CLOUD-1171, Done), but nothing in the tree WRITES a recordcrates/batten/src/tools.rs only reads .git/batten-tools/<tool>‖<version>‖<digest>, and the sole writer is crates/batten/tests/tool_verdict_facts.rs. So policy/validator-verdict-clean.rego resolves null and decides nothing today. The gap is a producer verb, not a fact
transcript content (finding-sink-check, board-payloads) GENUINELY BLOCKED CLOUD-1172 landed a COUNT, and a count cannot express a per-turn join with a negated arm. CLOUD-1254 owns the remainder
engine state (unlanded-check) UNBLOCKED, and named in the schema CLOUD-1203 Done; input.tree.state's own description names this program. Take it first
stranded UNBLOCKED CLOUD-1156 Done

Three classes remain open, and only three: CLOUD-1251 (a discovered out-of-root
set), CLOUD-1254 (transcript content), and the tool-verdict producer. Everything else
on this row is a successor waiting to be written.

The seconds in the old table are also stale — they predate CLOUD-1198's build-once
change and are wrong in both directions by 2–4x. Read every figure from
bench/suites/RESULTS.md at HEAD, never from this body.

schema-check, config-deprecations, skill-check and reference-check retired in**
**PR #780 and are no longer members. They shelled out to batten itself, and folding that away WAS the port — the landed proof of the existing-verb home (CLOUD-1199).

Four programs have no suite

doctor-check, gh-preflight, payload-field, perfone ledger arm each, not two. All four sit inside larger units except perf.


Refinement — Ready (66 deltas; write successors for the ~30 that are unblocked)

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

  • Authority boundary (§1). Up to 66 PRs, one per unit. Each deletes its program and its suite and lands its successor in the home the section above assigns — preset, consumer module, an existing §2 verb, prior art, or a withdrawal. No new crates/batten verb. With **one **// carried: arm per deleted path, or CLOUD-1080's withdrawal arm where no successor is owed, and its $MUTANT_GATES entry and #MUTANT rows re-homed. mise.toml, hk.pkl and .github/workflows/** invoke these by task name and are ungoverned, so every repoint is free.

  • Computable predicate (§2). Per unit, conserved unchanged. A port that also fixes a defect cannot be shown to have conserved anything, so a defect found mid-port is filed, not folded in.

  • Each PR states its axis verdict (§2). Landable and expressible are independent, and a unit that is landable but not expressible must say so rather than producing a module that loads and decides nothing — CLOUD-845's dead-gate class.

  • Deliberately not in scope (§2). The blocked classes' preconditions — each is owned elsewhere (CLOUD-1154, CLOUD-1156, CLOUD-1160). Merging units. Changing any predicate.

  • **Effect (§3). **read for all but the producing programs (perf-record, board-payloads, dist-adjacent), which keep their existing class.

  • Output and exit (§5). Pointer-only per unit; exit follows the 0/1/2/3 table, could-not-look is 3.

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

  • Test obligation (§7). Over the compiled binary in crates/batten/tests/; **no **.bats file is added or edited. One arm per deleted path (CLOUD-908) — and one arm, not two, for the four programs with no suite. Shown able to fail per CLOUD-418 per unit, each with its anti-vacuity mirror. Mutated: every #MUTANT row re-homed, mutant-census green after each PR, never only at the end.

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

  • Weakens (§8) — the config-deprecations member only, and §1's "every repoint is free" is wrong for it. §1 names mise.toml, hk.pkl and .github/workflows/** as the invoking surfaces and calls them ungoverned. That holds for the other members; it is false for config-deprecations, which is invoked from a [[rule]] row in batten.toml — a protected path, judged by config-lint against origin/main. Retiring the program forces that row's glob and check onto the successor, and config-lint reads a changed rule predicate as a weakening.

    The gate is correct in its own terms: the row does stop firing on the path it named. What the change buys is strictly more coverage, not less — the shell gate ran only when its own file changed, and the successor (crates/batten/tests/config_deprecations.rs) runs under test:cargo on every verify and every CI run. The deprecation predicate itself does not move: batten config deprecations decides it before and after, and the baseline is still the newest release tag in version order.

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

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

    Recorded honestly: this clause was groomed after the work started, on 2026-08-31, once config-lint refused PR refactor(ci): retire config-deprecations, reference-check, schema-check and skill-check #780. batten.toml:274-288 records the same gate blocking the same smell for CLOUD-437, where the resolution was to back the change out instead. The alternative here — drop config-deprecations from the bundle and land the other three — was offered and declined; the claim receipt carries --bypass-sequence, which says the refinement was self-served rather than pre-groomed.

Acceptance

  • Every one of the 66 units is either retired or carries a recorded verdict naming its blocking class.
  • Every retired unit names its home, read from CLOUD-1174's table rather than decided per PR, and no PR adds a crates/batten verb.
  • No governed program in the tree is unaccounted for — this row plus CLOUD-1155, CLOUD-1159, CLOUD-1160, CLOUD-1161, CLOUD-1162, CLOUD-1163 and CLOUD-1145 covers all 132.
  • mutant-census green after every PR.
  • bench/suites/RESULTS.md regenerates and the recovered seconds are reported against the 251.3s this row claims.

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

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

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

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

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

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

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

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

The seconds are stale, and every rank moved

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

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

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

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


Why

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

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

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

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

The home each unit's decision lands in

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

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

Which of the eight are actually reachable

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

Acceptance

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

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

CLOUD-1203 Two small fact gaps nothing owns: the git INDEX bytes (`git show :<path>`, which `tracked` explicitly is NOT) and the engine's own state store — filed together for economy, decided separately

Why

Two families the census found, each too small for its own row and neither owned by any existing one. They are filed together for economy and decided separately — they share nothing but size, the same shape CLOUD-1162 uses for two unrelated units.

Unit A — the git INDEX bytes

**No fact exposes the index. Only the worktree. **Fact::Tracked's own doc comment says so and names the trap (facts.rs:420-427): "Not the git index, despite the token… the walk honours .gitignore" — and warns this is exactly how "a module author writes a predicate about the index and gets an answer about the checkout."

Fact::GitStatus gives paths that differ plus an uncommitted count (facts.rs:1383-1386), never staged bytes. Fact::Prospective is about-to-be-written bytes on the call surface, not staged bytes. There is no git show :<path> equivalent anywhere in the set.

Known call sites, all reading the index deliberately rather than the worktree:

  • lock-complete.sh:83 git show :mise.lock, :117 git show :mise.toml, :143 git show :$wf
  • timeout-check.sh and mise-action-floor.sh read the same way

The count is stated as call sites rather than as a member total, because the census reported two suites at 10.6s while naming three programs, and CLOUD-1166 is the rule against republishing a number I cannot reconcile. Derive it before quoting it.

**Why the distinction is load-bearing and not pedantry. **lock-complete is the pure "committed bytes only, no network, no write" gate — its whole design (.claude/rules/toolchain.md) is that it judges the commit, not the developer's working copy. A successor reading the worktree would answer a different question and pass over a staged-but-unsaved edit. That is a silent wrong answer, not a missing feature.

It also invalidates a tempting shortcut: lock-complete looks like the cheapest unblocked retirement in the mid-band (6.2s, no inbound caller, no network). It is not retirable until this fact exists.

Unit B — the engine's own state store

unlanded-check.sh:91 runs batten state list and reads the engine's on-disk state. Its own header (:11-20) argues for the plain listing over -J because a by-path hook gets no mise env and so no pinned jq — a detail that survives the port only if the fact carries the same fields.

One member, 1.9s. The cheapest possible dismissal in the campaign — and worth deciding rather than leaving as the last unaccounted program, since CLOUD-1174's table must account for every governed program exactly once.

The shape is close to something that already exists: Fact::Produced and Fact::Records are already out-of-tree, keyed reads on Surface::Check (facts.rs:672-690, 869-884). Ask first whether the state store is a Produced reading under another name before adding a variant — the campaign's recorded failure is adding substrate nothing needed.


Refinement — Ready (two independent decisions, one row)

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

  • Authority boundary (§1). Per unit: a Fact variant and its batten.toml declaration in crates/batten/src/facts.rs, acquisition in rules.rs under a declaration guard, schema/policy-input.schema.json regenerated never hand-edited, .claude/rules/policy-modules.md's key list. No mise-tasks/ program and no tests/**/*.bats is edited or added. Unit B lands no variant if the store turns out to be a Produced reading.
  • Computable predicate (§2). Unit A: a module reads the STAGED bytes of a declared path, and the answer differs from the worktree bytes when the two differ — that difference is the whole point and must be the test. Unit B: a module reads the engine's state listing, keyed so a listing from another branch does not answer.
  • Could-not-look is distinct from absent (§2). A path with no staged entry, an unreadable index, and an empty store are separate readings. Rego reads undefined as does not hold, so collapsing them ships a silently-off gate — CLOUD-845's class.
  • Deliberately not in scope (§2). Retiring lock-complete, timeout-check, mise-action-floor or unlanded-check — each is its own row. Widening tracked to mean the index, which would silently change every existing consumer's answer and is refused outright.
  • Effect (§3). read for both. In-process via gix for unit A, matching the existing isolated open (git.rs:12-13) — no spawn.
  • Output and exit (§5). The facts are input; consumers emit pointers. Could-not-look exits 3, never a false 2.
  • Commit / bump (§6). feat(facts) — patch until 0.1.0, per unit. Additive, confirmed by mise run semver.
  • Test obligation (§7). Over the compiled binary in crates/batten/tests/; no .bats added or edited. Shown able to fail per CLOUD-418. Unit A's discriminating case is the one that matters: stage a change, leave the worktree different, and assert the fact returns the STAGED bytes — without it the test is satisfied by a fact that reads the checkout, which is the exact trap Fact::Tracked's doc names. Unit B: a listing keyed to another branch does not answer.
  • Blockers (§8). None. relatedTo CLOUD-907 (the git fact family this extends), CLOUD-851 (the production axis unit B may already sit on), CLOUD-1174 (the table both fill).

Acceptance

  • Unit A: a module reads staged bytes and is shown to return something different from the worktree when the two differ — asserted, not described.
  • tracked is not widened; its existing consumers' answers are unchanged, asserted.
  • Unit B: the state store is either shown to be a Produced reading under another name — in which case no variant is added and the row says so — or gets its own keyed variant.
  • Unit A's true member count is derived before it is quoted anywhere.
  • Each unit records its verdict independently; one landing does not close the other.

Found by deriving the blocked classes from the tree rather than from ticket prose: twelve families exist and six had no row. These are the two smallest.

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

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

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

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

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

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

Three consequences, and the first kills its membership

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

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

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

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

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

What that does to this row's numbers

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

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

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

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

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


Why

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

Acceptance

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

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

Found by subtracting four dispatched bundles from the corpus and asking what was left: this
class was in none of them, and the session that built those bundles had recorded
`session-start.bats` as *"the largest number in the tree with no owning row"* — which was
wrong twice over, since s owns it and had already closed.
```
</p>
</details>
<details>
<summary><a href="https://linear.app/buttoninc/issue/CLOUD-1224/board-sweepsh-builds-its-callees-filename-from-a-loop-variable-so-no">CLOUD-1224 `board-sweep.sh` builds its callee's filename from a loop variable, so no substitution arm can reach it — the retirement admission has three shapes and this caller matches none</a></summary>
<p>

**Why**

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

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

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

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

* `truncates_a_retired_reference` — no, nothing is shortened;
* `repoints_at_the_declared_successor` ([CLOUD-1121](https://linear.app/buttoninc/issue/CLOUD-1121/the-capture-spine-recovers-a-payload-the-agent-has-already-paid-for-a)) — no, `contains(was, gone)` is false, the line carries no repo-relative path;
* `repoints_at_the_declared_invocation` ([CLOUD-1219](https://linear.app/buttoninc/issue/CLOUD-1219/a-retirement-ledger-can-only-declare-a-successor-path-so-a-shell)) — no, `is_retired_reference` cannot resolve `${gate}.sh` to any deleted path.

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

## What it costs, concretely

`board-sweep.sh` drives the board gates by name. Retiring **any** of them forces an edit here, and that edit is `V-SHELL-RULE-EDITED` with one route (`R-PORT-AND-RETIRE`), no override and no `bypass_env`. So the board-gate bundle is not landable one gate at a time while this stands — which is the same structural block [CLOUD-1149](https://linear.app/buttoninc/issue/CLOUD-1149/repoints-at-the-declared-successor-cannot-match-how-bash-spells-a) measured for the sibling spelling and [CLOUD-1051](https://linear.app/buttoninc/issue/CLOUD-1051/replace-prose-only-and-filed-here-override-passwords-with-issued) for the declaration table, one shape further on.

[CLOUD-1221](https://linear.app/buttoninc/issue/CLOUD-1221/mise-tasksready-lintsh-is-a-729-line-second-implementation-rather-than) retired `ready-lint.sh` and did **not** hit this, which is worth stating because it bounds the row: `board-sweep.sh` reaches `ready-lint` transitively through `graph-check`, never by building its name. The loop at `:229-230` is over the gates `board-sweep` invokes directly.

## Two shapes could close it, and they are not equivalent

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

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

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

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

---

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

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

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

**Acceptance**

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

Found while landing [CLOUD-1149](https://linear.app/buttoninc/issue/CLOUD-1149/repoints-at-the-declared-successor-cannot-match-how-bash-spells-a)/[CLOUD-1219](https://linear.app/buttoninc/issue/CLOUD-1219/a-retirement-ledger-can-only-declare-a-successor-path-so-a-shell): the admission was widened twice and this caller still matched nothing, which was recorded in a PR body and nowhere the board could see.
</p>
</details>
<!-- linear-review-link -->
<p><a href="https://linear.app/buttoninc/review/fix-the-could-not-look-channel-and-the-punts-it-was-blocking-1d7a56496ccd">Review in Linear</a></p>

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 746a9094-86fc-49ed-b435-418de2194da1

📥 Commits

Reviewing files that changed from the base of the PR and between 69cf78a and 0511b04.

⛔ Files ignored due to path filters (1)
  • hk.pkl is excluded by !**/*.pkl
📒 Files selected for processing (20)
  • .claude/rules/policy-modules.md
  • .claude/rules/toolchain.md
  • batten.toml
  • bench/suites/RESULTS.md
  • crates/batten/src/lib.rs
  • crates/batten/src/prune.rs
  • crates/batten/src/rules.rs
  • crates/batten/tests/it/lock_complete.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/rules_drift.rs
  • crates/batten/tests/it/staged_facts.rs
  • crates/batten/tests/it/stop_posture.rs
  • mise-tasks/lock-complete.sh
  • mise-tasks/unlanded-check.sh
  • mise.toml
  • policy/lock-complete.rego
  • policy/lock-entry-complete.rego
  • policy/rules-drift.rego
  • tests/lock-complete.bats
  • tests/unlanded-check.bats
💤 Files with no reviewable changes (5)
  • mise-tasks/unlanded-check.sh
  • mise-tasks/lock-complete.sh
  • tests/unlanded-check.bats
  • tests/lock-complete.bats
  • policy/lock-entry-complete.rego

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


📝 Walkthrough

Walkthrough

The change adds the lock-complete policy and replaces the retired lock-entry task and test suite. Policy evaluation now supports declared staged formats, missing-input findings, mutation operations, and admission adjudication. Rules-drift checks Rego arm counts and schema keys. Retirement validation supports ported subjects. Stop-hook detection reads in-process findings. Journal pruning tracks capped observations and lowers capped floors after leaner closes.

Merge Risk: ⚪ Minimal · up to 0511b

The PR updates policy evaluation, lockfile handling, pruning behavior, and related retirements; 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 added a commit that referenced this pull request Sep 1, 2026
CLOUD-1049's second half, and it revives a gate that was registered on `main`
and deciding nothing.

`Format::for_path` searches `Format::extensions()`; no variant owns `lock`. So
a `staged` path ending in `.lock` was `NotAcquired::UnknownFormat` before a byte
was read, `input.tree.staged` never resolved it, Rego read undefined as *does
not hold*, and `policy/lock-entry-complete.rego`'s violation set was empty on
every run. Its four `test_` rules passed throughout, because `with input as`
fabricates the shape the engine could not produce -- and the module's own header
already named `staged_facts.rs` as the tier that would catch it.

`format` becomes a permitted policy column, consulted ONLY where the extension
names nothing. A fallback rather than an override, which is what keeps
`facts.rs`'s "the extension is the honest default there" true: a row cannot
re-label a `.json` as TOML and then blame the file for the parse failure.
Guessing stays refused; declaring does not.

Deliberately not extended to `documents`: an unknown-extension document is
already a hard config fault at load, which is a designed answer and a louder
one. `staged` never had that check, which is where the silence landed.

Four cases, and the pairs are what make them mean anything:

* a declared format resolves a staged path the extension cannot -- byte-identical
  fixture to the could-not-look case above it, one line added to the row, so the
  COLUMN is shown doing the work rather than the file changing
* a declared format does NOT override an extension that names one -- a `.toml`
  path with `format = "json5"` declared still reads as TOML, which is the bound
  the comment claims and would otherwise be unverified
* the committed `lock-entry-complete` module refuses a partial entry over the
  compiled binary -- a checksum with nothing to fetch, the exact defect its row
  was written for
* and passes a complete one, because a newly-live gate's likeliest failure is
  refusing everything, and nothing had ever shown this predicate discriminating

`no_artifact_name_reaches_the_core` caught a first draft of the column's comment
naming a consumer's lockfile inside `crates/batten`. It was right -- the core
knows formats, and which path carries which is the consumer's `batten.toml`
(rule 1, CLOUD-772) -- so the comment names the lockfile family instead, which
is also the truer claim.

3612 cargo tests, 42 policy bundles / 461 cases, batten-check clean.

Refs: CLOUD-1049, CLOUD-1203, CLOUD-772, CLOUD-843

Admits: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 00ffe68
Admits-epoch: 21b305197781cd9462881669ad90aeec429c80b0a323cbbc6bc150443061b6e6
Admits-author: alec@wenzowski.com
Admits-prev: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-answer-lost: policy/lock-entry-complete.rego stays a dead gate: registered, loading clean, passing its own load-time tier, and enforcing nothing on any run. A partial lockfile entry -- a checksum with nothing to fetch, the exact shape mise never repairs and which shipped once already -- goes unrefused indefinitely.
Admits-answer-precondition: The change adds a `format` column to one existing [[rule]] row so a policy module can read a staged path whose extension names no format. batten.toml IS the owning surface for a rule declaration -- there is nowhere else a row's columns can be written -- and the row in question (lock-entry-complete) has been registered and deciding nothing on every run because of the missing declaration. The write is one line added to an existing block, visible in the diff of PR #808 where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml is that owning surface: a [[rule]] column cannot be declared in the module, and the engine refuses `format` on a policy row until this change permits it. R-RESTORE-IT does not apply because the write is the deliberate content of the fix rather than damage to undo -- restoring the file returns the row to deciding nothing, which is the defect.
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch from db63217 to a67fffa Compare September 1, 2026 14:02
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-1049's second half, and it revives a gate that was registered on `main`
and deciding nothing.

`Format::for_path` searches `Format::extensions()`; no variant owns `lock`. So
a `staged` path ending in `.lock` was `NotAcquired::UnknownFormat` before a byte
was read, `input.tree.staged` never resolved it, Rego read undefined as *does
not hold*, and `policy/lock-entry-complete.rego`'s violation set was empty on
every run. Its four `test_` rules passed throughout, because `with input as`
fabricates the shape the engine could not produce -- and the module's own header
already named `staged_facts.rs` as the tier that would catch it.

`format` becomes a permitted policy column, consulted ONLY where the extension
names nothing. A fallback rather than an override, which is what keeps
`facts.rs`'s "the extension is the honest default there" true: a row cannot
re-label a `.json` as TOML and then blame the file for the parse failure.
Guessing stays refused; declaring does not.

Deliberately not extended to `documents`: an unknown-extension document is
already a hard config fault at load, which is a designed answer and a louder
one. `staged` never had that check, which is where the silence landed.

Four cases, and the pairs are what make them mean anything:

* a declared format resolves a staged path the extension cannot -- byte-identical
  fixture to the could-not-look case above it, one line added to the row, so the
  COLUMN is shown doing the work rather than the file changing
* a declared format does NOT override an extension that names one -- a `.toml`
  path with `format = "json5"` declared still reads as TOML, which is the bound
  the comment claims and would otherwise be unverified
* the committed `lock-entry-complete` module refuses a partial entry over the
  compiled binary -- a checksum with nothing to fetch, the exact defect its row
  was written for
* and passes a complete one, because a newly-live gate's likeliest failure is
  refusing everything, and nothing had ever shown this predicate discriminating

`no_artifact_name_reaches_the_core` caught a first draft of the column's comment
naming a consumer's lockfile inside `crates/batten`. It was right -- the core
knows formats, and which path carries which is the consumer's `batten.toml`
(rule 1, CLOUD-772) -- so the comment names the lockfile family instead, which
is also the truer claim.

3612 cargo tests, 42 policy bundles / 461 cases, batten-check clean.

Refs: CLOUD-1049, CLOUD-1203, CLOUD-772, CLOUD-843

Admits: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 00ffe68
Admits-epoch: 21b305197781cd9462881669ad90aeec429c80b0a323cbbc6bc150443061b6e6
Admits-author: alec@wenzowski.com
Admits-prev: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-answer-lost: policy/lock-entry-complete.rego stays a dead gate: registered, loading clean, passing its own load-time tier, and enforcing nothing on any run. A partial lockfile entry -- a checksum with nothing to fetch, the exact shape mise never repairs and which shipped once already -- goes unrefused indefinitely.
Admits-answer-precondition: The change adds a `format` column to one existing [[rule]] row so a policy module can read a staged path whose extension names no format. batten.toml IS the owning surface for a rule declaration -- there is nowhere else a row's columns can be written -- and the row in question (lock-entry-complete) has been registered and deciding nothing on every run because of the missing declaration. The write is one line added to an existing block, visible in the diff of PR #808 where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml is that owning surface: a [[rule]] column cannot be declared in the module, and the engine refuses `format` on a policy row until this change permits it. R-RESTORE-IT does not apply because the write is the deliberate content of the fix rather than damage to undo -- restoring the file returns the row to deciding nothing, which is the defect.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… read

CLOUD-843's member 7, and the largest single unit in the bundle: 419 lines of
bash and a 37-case suite become `policy/lock-complete.rego`, eight predicates
over `input.tree.staged` plus one over the line surface.

WHAT UNBLOCKED IT is the `format` column landed one commit back. `mise.lock`
carries an extension no `Format` owns, so `Format::for_path` refused it before a
byte was read and `input.tree.staged["mise.lock"]` never resolved. That was not
hypothetical: `policy/lock-entry-complete.rego` was registered on `main`,
loading clean, passing its own load-time tier and enforcing nothing on every run
for exactly that reason.

THE PARSED NODE REPLACES 80 LINES OF `awk`. The predecessor read
`[[tools.<name>]]` and `[tools.<name>."platforms.<p>"]` with a hand-rolled
record stream and a second pass over `mise.toml`'s `[tools]` table; both are a
map lookup here, and the quoted-versus-bare tool name that needed a `gsub` is
just a key.

AND ONE MODULE IS SUBSUMED RATHER THAN LEFT BESIDE IT.
`policy/lock-entry-complete.rego` decided a strict subset of predicate 2 — a
platform block with a checksum and nothing to fetch — and two rows over one
question is the second authority this repository refuses everywhere else. Its
verdict token `V-LOCK-ENTRY-PARTIAL` is raised by the new module, so the
registry row is conserved rather than retired, and its two cases in
`staged_facts.rs` are repointed. That file keeps them because the ENGINE
property they assert is its own subject: that a declared `staged` path reaches a
registered row at all.

THE SPLIT BETWEEN `staged` AND `line_sources` IS DELIBERATE, and it is the one
place a verdict reads the working tree. Every lockfile and manifest decision
comes off the index, which is CLOUD-227's whole point: a cold `mise install`
rewrites `mise.lock` behind the author's back, so a gate reading the checkout
was red in every agent sandbox and green in the one CI job that runs it, for the
same commit. The workflow clause cannot use `staged` — `git::staged_facts` looks
each declared path up in the index BY NAME, so a workflow set could only reach
it as a hand-maintained inventory, and a workflow missing from that list is a
silent hole in exactly the gate that exists to close one. A glob over the line
surface has no such hole, and nothing but a person writes a workflow file.

Nine of the thirty-seven cases could not be carried verbatim and each carries
its reason on its arm. The load-bearing ones:

* `BATTEN_LOCK_PLATFORMS` is gone rather than ported — a module reads no
  environment, and the set a repository installs on is committed config rather
  than a knowable string anyone can spend to make the gate agree with them
* both exit codes are 2 now, which is the one exit table with no per-verb
  exception; the DISTINCTION survives as `V-LOCK-UNREADABLE`, its own token with
  its own remedy
* the could-not-look arm is conditioned on a staged `mise.toml` declaring
  `[tools]`, because a `[[rule]]` has no call site and an unconditional refusal
  would speak in every fixture repository inheriting this config — CLOUD-1164's
  finding avoided rather than survived

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-843, CLOUD-1203, CLOUD-223, CLOUD-227, CLOUD-281, CLOUD-333, CLOUD-593, CLOUD-1164

Admits: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bc12e3d
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-answer-lost: The whole of member 7. mise-tasks/lock-complete.sh stays alive as 419 lines of bash with a 37-case bats suite that CLOUD-843 exists to retire, and policy/lock-entry-complete.rego stays beside it as a second authority over the same question deciding a strict subset of it. Nothing else can carry the predicate: policy/lock-complete.rego is inert without a [[rule]] row naming it, and the nine V-LOCK-* verdicts are worse than inert, because a module raising a token no [[verdict]] row declares refuses the module load outright, so the module would not load at all.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-843's retirement of mise-tasks/lock-complete.sh replaces a shell program with a [[rule]] row plus nine [[verdict]] rows and one [[pattern]] row, and a rule declaration, a verdict token and a pattern id are readable by the engine from exactly one place by construction: `Rule` carries deny_unknown_fields so an undeclared column is a load error, and the verdict registry is held in BOTH directions so a module raising a token no row declares fails to load while a row nothing raises fails it too. The fact satisfying the precondition: the write is a rule row, its verdict registry and one pattern in the one file that can hold them, and it lands in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
wenzowski added a commit that referenced this pull request Sep 1, 2026
…r mechanisms

Two predicates `rules-drift` owed and did not have, and each was measured against
a claim that was live in this tree when it was written.

CLOUD-1150 §2 — `restated-arm-count-drifts`. Predicate 1 one shape up: that one
holds a restated VALUE to the mechanism, this holds a restated CLOSED COUNT over
a set of arms the mechanism owns. The cost of not having it is on the row —
`.claude/rules/toolchain.md` said "One edit is admitted, and only one" while
`shell-retirement.rego` carried three, a grooming session read the sentence,
concluded three governed programs were permanently unretirable, and wrote that
into two dispatched agent prompts and five issue bodies before anyone read the
module. **The same file was wrong again when this landed**: it said "three
shapes on its addition side" and `admitted_addition` carries four, CLOUD-1224's
`drops_a_retired_name` having arrived unmentioned. The gate caught its own file
on its first run, which is the honest way to show it discriminates.

The construction is `` `name` (N arms) `` and the authority is the modules
themselves — every rule HEAD at column zero across `policy/*.rego`, a sixth
acquisition surface for this row. A hand-written table of the arms would be the
third authority the whole gate exists to refuse. The anti-inversion bound is
predicate 1's, one shape up: prose naming a rule without asserting how many arms
it has is untouched, because this file's own rule is that a value should be read
at its authority rather than restated.

CLOUD-1206 — `schema-key-undocumented`. Predicate 3 RUN BACKWARDS: that one
refuses a named key the engine cannot emit, this refuses an emittable key the
file claiming to enumerate them does not name. Measured 2026-08-30, the schema
declared `base-delta` and `symbols` and `.claude/rules/policy-modules.md` named
neither, while that file's own closing sentence said `rules-drift` held the
lists to the schemas. A false assurance is worse than an unclaimed gap, and
`base-delta` is the projection a Todo row was about to re-derive a diff for.

SCOPED TO THE FILE THAT MAKES THE CLAIM, which is what keeps it inside the
anti-restatement bound: demanding that every prose surface enumerate 24 tree
keys would be exactly the completeness pressure predicate 1's mirror refuses. A
file that says it holds the lists in both directions has made a claim that is
present and wrong when a key is missing; a file that says nothing is an ordinary
consumer and is silent.

ONE ARM ADDED TO `named_keys`, and it is load-bearing rather than tidy. Eight of
the engine's keys carry a hyphen, which is not a legal Rego selector, so they
are only ever written `input.tree["git-history"]` — and `policy-input-key`'s
`\.` cannot match a `[`. Left unread this was a hole in BOTH directions at once:
predicate 3 could not judge a subscripted key the engine cannot emit, and this
predicate's first run over the real tree would have been eight false findings,
which is a gate nobody keeps.

Both anchors are line-oriented over prose `prettier` owns, so
`the_two_anchors_this_gate_keys_on_are_still_one_line_in_the_committed_files`
asserts over the committed files that a reflow has not pushed either across a
line break. A dead gate and a clean tree are byte-identical on the decision
surface; that case is the one thing standing between this gate and its own
opening paragraph.

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-1150, CLOUD-1206, CLOUD-506, CLOUD-770, CLOUD-932, CLOUD-845, CLOUD-1224

Admits: a44af27cfa26d1a56eefb053a30f786948d6396a021b2d2c86c1c1b1cdf04b12
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 34ec6f3
Admits-epoch: bdbfe1b93de75bd70e33efc459d9b94cc7d67e6ef40f3573c283b01014d31546
Admits-author: alec@wenzowski.com
Admits-prev: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-answer-lost: Both predicates go dead in the worst way — loading clean and deciding nothing, which is the class `.claude/rules/policy-modules.md` opens with. Without the `policy/*.rego` line source the arm-count predicate reads an empty head set and can never fire; without the pattern rows every `regex.find_n` resolves to undefined; without the verdict rows the module does not load at all, taking the four existing predicates with it. Concretely: `.claude/rules/toolchain.md` keeps saying `admitted_addition` has three arms while the module carries four — the same shape of false premise that a grooming session already wrote into two dispatched agent prompts and five issue bodies at "one" versus three — and `.claude/rules/policy-modules.md` keeps promising a both-directions guarantee it does not have.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-1150 §2 and CLOUD-1206 add two predicates to the registered `rules-drift` module, and a predicate is unreachable without three declarations that live in exactly one place by construction: the row's `line_sources` must name `policy/*.rego` or the engine hands the module no rego lines and `arm_count` is undefined; four `[[pattern]]` rows must exist or the module's `data.batten.patterns[...]` reads resolve to undefined, since an inline regex is refused at load; and two `[[verdict]]` rows must exist or the module raising an undeclared token fails to load outright, in both directions. The fact satisfying the precondition: the write is one glob added to an existing row plus four pattern rows and two verdict rows, in the one file that can hold them, landing in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
Admits: 9db820b57d821f1ebb1914a1913ee66c49ffe7a31c522b3400e917a39dd60a8e
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/rules-drift.rego
Admits-head: 34ec6f3
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Both predicates, and the live defects each was measured against. CLOUD-1150 §2: `.claude/rules/toolchain.md` says how many arms `admitted_addition` has and has been wrong twice — measured once at "one" when the module carried three, a claim a grooming session then wrote into two dispatched agent prompts and five issue bodies, and again at "three" when the module carries four. CLOUD-1206: `.claude/rules/policy-modules.md` promises in its own words that the key lists are held to the generated schemas, and only one direction was ever checked — measured 2026-08-30 the schema declared `base-delta` and `symbols` and the file named neither, with `base-delta` the projection a Todo row was about to re-derive a diff for. Neither predicate is expressible outside this module.
Admits-answer-precondition: policy/rules-drift.rego IS the surface this class names, and there is nowhere else the change can be written. CLOUD-1150 §2 and CLOUD-1206 are two predicates over prose claims — a restated `(N arms)` count held to the modules' own rule heads, and a schema key the file claiming to enumerate the key set does not name — and `rules-drift` is the registered module that already owns this class ("a value a rules file restates still agrees with the mechanism that owns it"). A second module over the same class would be the third authority the gate itself exists to refuse, and a predicate cannot live anywhere but in the module its `[[rule]]` row names. The fact satisfying the precondition: the write adds two `rules contains` declarations, two `violation` arms, one extra `named_keys` arm and eight `test_` rules to the module that already decides this class, and it lands in PR #808 where the diff is in front of every reviewer and review bot; `mise run policy-test` checks it before it lands, which is the route this class's own remedy names.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because policy/rules-drift.rego IS the owning surface for a rules-drift predicate. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file; a `.rego` module has no such tool, and its own [[redirect]] row says the remedy is "change it in a pull request" with `mise run policy-test` as the check — which is exactly this: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, policy-test green at 43 bundles / 519 cases, and a compiled tier in crates/batten/tests/it/rules_drift.rs driving the committed module over the real boundary. R-RESTORE-IT does not apply: nothing here is a mistaken write to undo — restoring the file removes the two predicates, which is the defect rather than the repair.
@wenzowski wenzowski changed the title Fix the could-not-look channel, and the punts it was blocking Fix the could-not-look channel, and every punt it was blocking Sep 1, 2026
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch from a67fffa to f0c8353 Compare September 1, 2026 14:34
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-1049's second half, and it revives a gate that was registered on `main`
and deciding nothing.

`Format::for_path` searches `Format::extensions()`; no variant owns `lock`. So
a `staged` path ending in `.lock` was `NotAcquired::UnknownFormat` before a byte
was read, `input.tree.staged` never resolved it, Rego read undefined as *does
not hold*, and `policy/lock-entry-complete.rego`'s violation set was empty on
every run. Its four `test_` rules passed throughout, because `with input as`
fabricates the shape the engine could not produce -- and the module's own header
already named `staged_facts.rs` as the tier that would catch it.

`format` becomes a permitted policy column, consulted ONLY where the extension
names nothing. A fallback rather than an override, which is what keeps
`facts.rs`'s "the extension is the honest default there" true: a row cannot
re-label a `.json` as TOML and then blame the file for the parse failure.
Guessing stays refused; declaring does not.

Deliberately not extended to `documents`: an unknown-extension document is
already a hard config fault at load, which is a designed answer and a louder
one. `staged` never had that check, which is where the silence landed.

Four cases, and the pairs are what make them mean anything:

* a declared format resolves a staged path the extension cannot -- byte-identical
  fixture to the could-not-look case above it, one line added to the row, so the
  COLUMN is shown doing the work rather than the file changing
* a declared format does NOT override an extension that names one -- a `.toml`
  path with `format = "json5"` declared still reads as TOML, which is the bound
  the comment claims and would otherwise be unverified
* the committed `lock-entry-complete` module refuses a partial entry over the
  compiled binary -- a checksum with nothing to fetch, the exact defect its row
  was written for
* and passes a complete one, because a newly-live gate's likeliest failure is
  refusing everything, and nothing had ever shown this predicate discriminating

`no_artifact_name_reaches_the_core` caught a first draft of the column's comment
naming a consumer's lockfile inside `crates/batten`. It was right -- the core
knows formats, and which path carries which is the consumer's `batten.toml`
(rule 1, CLOUD-772) -- so the comment names the lockfile family instead, which
is also the truer claim.

3612 cargo tests, 42 policy bundles / 461 cases, batten-check clean.

Refs: CLOUD-1049, CLOUD-1203, CLOUD-772, CLOUD-843

Admits: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 00ffe68
Admits-epoch: 21b305197781cd9462881669ad90aeec429c80b0a323cbbc6bc150443061b6e6
Admits-author: alec@wenzowski.com
Admits-prev: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-answer-lost: policy/lock-entry-complete.rego stays a dead gate: registered, loading clean, passing its own load-time tier, and enforcing nothing on any run. A partial lockfile entry -- a checksum with nothing to fetch, the exact shape mise never repairs and which shipped once already -- goes unrefused indefinitely.
Admits-answer-precondition: The change adds a `format` column to one existing [[rule]] row so a policy module can read a staged path whose extension names no format. batten.toml IS the owning surface for a rule declaration -- there is nowhere else a row's columns can be written -- and the row in question (lock-entry-complete) has been registered and deciding nothing on every run because of the missing declaration. The write is one line added to an existing block, visible in the diff of PR #808 where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml is that owning surface: a [[rule]] column cannot be declared in the module, and the engine refuses `format` on a policy row until this change permits it. R-RESTORE-IT does not apply because the write is the deliberate content of the fix rather than damage to undo -- restoring the file returns the row to deciding nothing, which is the defect.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… read

CLOUD-843's member 7, and the largest single unit in the bundle: 419 lines of
bash and a 37-case suite become `policy/lock-complete.rego`, eight predicates
over `input.tree.staged` plus one over the line surface.

WHAT UNBLOCKED IT is the `format` column landed one commit back. `mise.lock`
carries an extension no `Format` owns, so `Format::for_path` refused it before a
byte was read and `input.tree.staged["mise.lock"]` never resolved. That was not
hypothetical: `policy/lock-entry-complete.rego` was registered on `main`,
loading clean, passing its own load-time tier and enforcing nothing on every run
for exactly that reason.

THE PARSED NODE REPLACES 80 LINES OF `awk`. The predecessor read
`[[tools.<name>]]` and `[tools.<name>."platforms.<p>"]` with a hand-rolled
record stream and a second pass over `mise.toml`'s `[tools]` table; both are a
map lookup here, and the quoted-versus-bare tool name that needed a `gsub` is
just a key.

AND ONE MODULE IS SUBSUMED RATHER THAN LEFT BESIDE IT.
`policy/lock-entry-complete.rego` decided a strict subset of predicate 2 — a
platform block with a checksum and nothing to fetch — and two rows over one
question is the second authority this repository refuses everywhere else. Its
verdict token `V-LOCK-ENTRY-PARTIAL` is raised by the new module, so the
registry row is conserved rather than retired, and its two cases in
`staged_facts.rs` are repointed. That file keeps them because the ENGINE
property they assert is its own subject: that a declared `staged` path reaches a
registered row at all.

THE SPLIT BETWEEN `staged` AND `line_sources` IS DELIBERATE, and it is the one
place a verdict reads the working tree. Every lockfile and manifest decision
comes off the index, which is CLOUD-227's whole point: a cold `mise install`
rewrites `mise.lock` behind the author's back, so a gate reading the checkout
was red in every agent sandbox and green in the one CI job that runs it, for the
same commit. The workflow clause cannot use `staged` — `git::staged_facts` looks
each declared path up in the index BY NAME, so a workflow set could only reach
it as a hand-maintained inventory, and a workflow missing from that list is a
silent hole in exactly the gate that exists to close one. A glob over the line
surface has no such hole, and nothing but a person writes a workflow file.

Nine of the thirty-seven cases could not be carried verbatim and each carries
its reason on its arm. The load-bearing ones:

* `BATTEN_LOCK_PLATFORMS` is gone rather than ported — a module reads no
  environment, and the set a repository installs on is committed config rather
  than a knowable string anyone can spend to make the gate agree with them
* both exit codes are 2 now, which is the one exit table with no per-verb
  exception; the DISTINCTION survives as `V-LOCK-UNREADABLE`, its own token with
  its own remedy
* the could-not-look arm is conditioned on a staged `mise.toml` declaring
  `[tools]`, because a `[[rule]]` has no call site and an unconditional refusal
  would speak in every fixture repository inheriting this config — CLOUD-1164's
  finding avoided rather than survived

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-843, CLOUD-1203, CLOUD-223, CLOUD-227, CLOUD-281, CLOUD-333, CLOUD-593, CLOUD-1164

Admits: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bc12e3d
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-answer-lost: The whole of member 7. mise-tasks/lock-complete.sh stays alive as 419 lines of bash with a 37-case bats suite that CLOUD-843 exists to retire, and policy/lock-entry-complete.rego stays beside it as a second authority over the same question deciding a strict subset of it. Nothing else can carry the predicate: policy/lock-complete.rego is inert without a [[rule]] row naming it, and the nine V-LOCK-* verdicts are worse than inert, because a module raising a token no [[verdict]] row declares refuses the module load outright, so the module would not load at all.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-843's retirement of mise-tasks/lock-complete.sh replaces a shell program with a [[rule]] row plus nine [[verdict]] rows and one [[pattern]] row, and a rule declaration, a verdict token and a pattern id are readable by the engine from exactly one place by construction: `Rule` carries deny_unknown_fields so an undeclared column is a load error, and the verdict registry is held in BOTH directions so a module raising a token no row declares fails to load while a row nothing raises fails it too. The fact satisfying the precondition: the write is a rule row, its verdict registry and one pattern in the one file that can hold them, and it lands in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
wenzowski added a commit that referenced this pull request Sep 1, 2026
…r mechanisms

Two predicates `rules-drift` owed and did not have, and each was measured against
a claim that was live in this tree when it was written.

CLOUD-1150 §2 — `restated-arm-count-drifts`. Predicate 1 one shape up: that one
holds a restated VALUE to the mechanism, this holds a restated CLOSED COUNT over
a set of arms the mechanism owns. The cost of not having it is on the row —
`.claude/rules/toolchain.md` said "One edit is admitted, and only one" while
`shell-retirement.rego` carried three, a grooming session read the sentence,
concluded three governed programs were permanently unretirable, and wrote that
into two dispatched agent prompts and five issue bodies before anyone read the
module. **The same file was wrong again when this landed**: it said "three
shapes on its addition side" and `admitted_addition` carries four, CLOUD-1224's
`drops_a_retired_name` having arrived unmentioned. The gate caught its own file
on its first run, which is the honest way to show it discriminates.

The construction is `` `name` (N arms) `` and the authority is the modules
themselves — every rule HEAD at column zero across `policy/*.rego`, a sixth
acquisition surface for this row. A hand-written table of the arms would be the
third authority the whole gate exists to refuse. The anti-inversion bound is
predicate 1's, one shape up: prose naming a rule without asserting how many arms
it has is untouched, because this file's own rule is that a value should be read
at its authority rather than restated.

CLOUD-1206 — `schema-key-undocumented`. Predicate 3 RUN BACKWARDS: that one
refuses a named key the engine cannot emit, this refuses an emittable key the
file claiming to enumerate them does not name. Measured 2026-08-30, the schema
declared `base-delta` and `symbols` and `.claude/rules/policy-modules.md` named
neither, while that file's own closing sentence said `rules-drift` held the
lists to the schemas. A false assurance is worse than an unclaimed gap, and
`base-delta` is the projection a Todo row was about to re-derive a diff for.

SCOPED TO THE FILE THAT MAKES THE CLAIM, which is what keeps it inside the
anti-restatement bound: demanding that every prose surface enumerate 24 tree
keys would be exactly the completeness pressure predicate 1's mirror refuses. A
file that says it holds the lists in both directions has made a claim that is
present and wrong when a key is missing; a file that says nothing is an ordinary
consumer and is silent.

ONE ARM ADDED TO `named_keys`, and it is load-bearing rather than tidy. Eight of
the engine's keys carry a hyphen, which is not a legal Rego selector, so they
are only ever written `input.tree["git-history"]` — and `policy-input-key`'s
`\.` cannot match a `[`. Left unread this was a hole in BOTH directions at once:
predicate 3 could not judge a subscripted key the engine cannot emit, and this
predicate's first run over the real tree would have been eight false findings,
which is a gate nobody keeps.

Both anchors are line-oriented over prose `prettier` owns, so
`the_two_anchors_this_gate_keys_on_are_still_one_line_in_the_committed_files`
asserts over the committed files that a reflow has not pushed either across a
line break. A dead gate and a clean tree are byte-identical on the decision
surface; that case is the one thing standing between this gate and its own
opening paragraph.

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-1150, CLOUD-1206, CLOUD-506, CLOUD-770, CLOUD-932, CLOUD-845, CLOUD-1224

Admits: a44af27cfa26d1a56eefb053a30f786948d6396a021b2d2c86c1c1b1cdf04b12
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 34ec6f3
Admits-epoch: bdbfe1b93de75bd70e33efc459d9b94cc7d67e6ef40f3573c283b01014d31546
Admits-author: alec@wenzowski.com
Admits-prev: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-answer-lost: Both predicates go dead in the worst way — loading clean and deciding nothing, which is the class `.claude/rules/policy-modules.md` opens with. Without the `policy/*.rego` line source the arm-count predicate reads an empty head set and can never fire; without the pattern rows every `regex.find_n` resolves to undefined; without the verdict rows the module does not load at all, taking the four existing predicates with it. Concretely: `.claude/rules/toolchain.md` keeps saying `admitted_addition` has three arms while the module carries four — the same shape of false premise that a grooming session already wrote into two dispatched agent prompts and five issue bodies at "one" versus three — and `.claude/rules/policy-modules.md` keeps promising a both-directions guarantee it does not have.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-1150 §2 and CLOUD-1206 add two predicates to the registered `rules-drift` module, and a predicate is unreachable without three declarations that live in exactly one place by construction: the row's `line_sources` must name `policy/*.rego` or the engine hands the module no rego lines and `arm_count` is undefined; four `[[pattern]]` rows must exist or the module's `data.batten.patterns[...]` reads resolve to undefined, since an inline regex is refused at load; and two `[[verdict]]` rows must exist or the module raising an undeclared token fails to load outright, in both directions. The fact satisfying the precondition: the write is one glob added to an existing row plus four pattern rows and two verdict rows, in the one file that can hold them, landing in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
Admits: 9db820b57d821f1ebb1914a1913ee66c49ffe7a31c522b3400e917a39dd60a8e
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/rules-drift.rego
Admits-head: 34ec6f3
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Both predicates, and the live defects each was measured against. CLOUD-1150 §2: `.claude/rules/toolchain.md` says how many arms `admitted_addition` has and has been wrong twice — measured once at "one" when the module carried three, a claim a grooming session then wrote into two dispatched agent prompts and five issue bodies, and again at "three" when the module carries four. CLOUD-1206: `.claude/rules/policy-modules.md` promises in its own words that the key lists are held to the generated schemas, and only one direction was ever checked — measured 2026-08-30 the schema declared `base-delta` and `symbols` and the file named neither, with `base-delta` the projection a Todo row was about to re-derive a diff for. Neither predicate is expressible outside this module.
Admits-answer-precondition: policy/rules-drift.rego IS the surface this class names, and there is nowhere else the change can be written. CLOUD-1150 §2 and CLOUD-1206 are two predicates over prose claims — a restated `(N arms)` count held to the modules' own rule heads, and a schema key the file claiming to enumerate the key set does not name — and `rules-drift` is the registered module that already owns this class ("a value a rules file restates still agrees with the mechanism that owns it"). A second module over the same class would be the third authority the gate itself exists to refuse, and a predicate cannot live anywhere but in the module its `[[rule]]` row names. The fact satisfying the precondition: the write adds two `rules contains` declarations, two `violation` arms, one extra `named_keys` arm and eight `test_` rules to the module that already decides this class, and it lands in PR #808 where the diff is in front of every reviewer and review bot; `mise run policy-test` checks it before it lands, which is the route this class's own remedy names.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because policy/rules-drift.rego IS the owning surface for a rules-drift predicate. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file; a `.rego` module has no such tool, and its own [[redirect]] row says the remedy is "change it in a pull request" with `mise run policy-test` as the check — which is exactly this: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, policy-test green at 43 bundles / 519 cases, and a compiled tier in crates/batten/tests/it/rules_drift.rs driving the committed module over the real boundary. R-RESTORE-IT does not apply: nothing here is a mistaken write to undo — restoring the file removes the two predicates, which is the defect rather than the repair.
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-1049's second half, and it revives a gate that was registered on `main`
and deciding nothing.

`Format::for_path` searches `Format::extensions()`; no variant owns `lock`. So
a `staged` path ending in `.lock` was `NotAcquired::UnknownFormat` before a byte
was read, `input.tree.staged` never resolved it, Rego read undefined as *does
not hold*, and `policy/lock-entry-complete.rego`'s violation set was empty on
every run. Its four `test_` rules passed throughout, because `with input as`
fabricates the shape the engine could not produce -- and the module's own header
already named `staged_facts.rs` as the tier that would catch it.

`format` becomes a permitted policy column, consulted ONLY where the extension
names nothing. A fallback rather than an override, which is what keeps
`facts.rs`'s "the extension is the honest default there" true: a row cannot
re-label a `.json` as TOML and then blame the file for the parse failure.
Guessing stays refused; declaring does not.

Deliberately not extended to `documents`: an unknown-extension document is
already a hard config fault at load, which is a designed answer and a louder
one. `staged` never had that check, which is where the silence landed.

Four cases, and the pairs are what make them mean anything:

* a declared format resolves a staged path the extension cannot -- byte-identical
  fixture to the could-not-look case above it, one line added to the row, so the
  COLUMN is shown doing the work rather than the file changing
* a declared format does NOT override an extension that names one -- a `.toml`
  path with `format = "json5"` declared still reads as TOML, which is the bound
  the comment claims and would otherwise be unverified
* the committed `lock-entry-complete` module refuses a partial entry over the
  compiled binary -- a checksum with nothing to fetch, the exact defect its row
  was written for
* and passes a complete one, because a newly-live gate's likeliest failure is
  refusing everything, and nothing had ever shown this predicate discriminating

`no_artifact_name_reaches_the_core` caught a first draft of the column's comment
naming a consumer's lockfile inside `crates/batten`. It was right -- the core
knows formats, and which path carries which is the consumer's `batten.toml`
(rule 1, CLOUD-772) -- so the comment names the lockfile family instead, which
is also the truer claim.

3612 cargo tests, 42 policy bundles / 461 cases, batten-check clean.

Refs: CLOUD-1049, CLOUD-1203, CLOUD-772, CLOUD-843

Admits: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 00ffe68
Admits-epoch: 21b305197781cd9462881669ad90aeec429c80b0a323cbbc6bc150443061b6e6
Admits-author: alec@wenzowski.com
Admits-prev: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-answer-lost: policy/lock-entry-complete.rego stays a dead gate: registered, loading clean, passing its own load-time tier, and enforcing nothing on any run. A partial lockfile entry -- a checksum with nothing to fetch, the exact shape mise never repairs and which shipped once already -- goes unrefused indefinitely.
Admits-answer-precondition: The change adds a `format` column to one existing [[rule]] row so a policy module can read a staged path whose extension names no format. batten.toml IS the owning surface for a rule declaration -- there is nowhere else a row's columns can be written -- and the row in question (lock-entry-complete) has been registered and deciding nothing on every run because of the missing declaration. The write is one line added to an existing block, visible in the diff of PR #808 where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml is that owning surface: a [[rule]] column cannot be declared in the module, and the engine refuses `format` on a policy row until this change permits it. R-RESTORE-IT does not apply because the write is the deliberate content of the fix rather than damage to undo -- restoring the file returns the row to deciding nothing, which is the defect.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… read

CLOUD-843's member 7, and the largest single unit in the bundle: 419 lines of
bash and a 37-case suite become `policy/lock-complete.rego`, eight predicates
over `input.tree.staged` plus one over the line surface.

WHAT UNBLOCKED IT is the `format` column landed one commit back. `mise.lock`
carries an extension no `Format` owns, so `Format::for_path` refused it before a
byte was read and `input.tree.staged["mise.lock"]` never resolved. That was not
hypothetical: `policy/lock-entry-complete.rego` was registered on `main`,
loading clean, passing its own load-time tier and enforcing nothing on every run
for exactly that reason.

THE PARSED NODE REPLACES 80 LINES OF `awk`. The predecessor read
`[[tools.<name>]]` and `[tools.<name>."platforms.<p>"]` with a hand-rolled
record stream and a second pass over `mise.toml`'s `[tools]` table; both are a
map lookup here, and the quoted-versus-bare tool name that needed a `gsub` is
just a key.

AND ONE MODULE IS SUBSUMED RATHER THAN LEFT BESIDE IT.
`policy/lock-entry-complete.rego` decided a strict subset of predicate 2 — a
platform block with a checksum and nothing to fetch — and two rows over one
question is the second authority this repository refuses everywhere else. Its
verdict token `V-LOCK-ENTRY-PARTIAL` is raised by the new module, so the
registry row is conserved rather than retired, and its two cases in
`staged_facts.rs` are repointed. That file keeps them because the ENGINE
property they assert is its own subject: that a declared `staged` path reaches a
registered row at all.

THE SPLIT BETWEEN `staged` AND `line_sources` IS DELIBERATE, and it is the one
place a verdict reads the working tree. Every lockfile and manifest decision
comes off the index, which is CLOUD-227's whole point: a cold `mise install`
rewrites `mise.lock` behind the author's back, so a gate reading the checkout
was red in every agent sandbox and green in the one CI job that runs it, for the
same commit. The workflow clause cannot use `staged` — `git::staged_facts` looks
each declared path up in the index BY NAME, so a workflow set could only reach
it as a hand-maintained inventory, and a workflow missing from that list is a
silent hole in exactly the gate that exists to close one. A glob over the line
surface has no such hole, and nothing but a person writes a workflow file.

Nine of the thirty-seven cases could not be carried verbatim and each carries
its reason on its arm. The load-bearing ones:

* `BATTEN_LOCK_PLATFORMS` is gone rather than ported — a module reads no
  environment, and the set a repository installs on is committed config rather
  than a knowable string anyone can spend to make the gate agree with them
* both exit codes are 2 now, which is the one exit table with no per-verb
  exception; the DISTINCTION survives as `V-LOCK-UNREADABLE`, its own token with
  its own remedy
* the could-not-look arm is conditioned on a staged `mise.toml` declaring
  `[tools]`, because a `[[rule]]` has no call site and an unconditional refusal
  would speak in every fixture repository inheriting this config — CLOUD-1164's
  finding avoided rather than survived

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-843, CLOUD-1203, CLOUD-223, CLOUD-227, CLOUD-281, CLOUD-333, CLOUD-593, CLOUD-1164

Admits: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bc12e3d
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-answer-lost: The whole of member 7. mise-tasks/lock-complete.sh stays alive as 419 lines of bash with a 37-case bats suite that CLOUD-843 exists to retire, and policy/lock-entry-complete.rego stays beside it as a second authority over the same question deciding a strict subset of it. Nothing else can carry the predicate: policy/lock-complete.rego is inert without a [[rule]] row naming it, and the nine V-LOCK-* verdicts are worse than inert, because a module raising a token no [[verdict]] row declares refuses the module load outright, so the module would not load at all.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-843's retirement of mise-tasks/lock-complete.sh replaces a shell program with a [[rule]] row plus nine [[verdict]] rows and one [[pattern]] row, and a rule declaration, a verdict token and a pattern id are readable by the engine from exactly one place by construction: `Rule` carries deny_unknown_fields so an undeclared column is a load error, and the verdict registry is held in BOTH directions so a module raising a token no row declares fails to load while a row nothing raises fails it too. The fact satisfying the precondition: the write is a rule row, its verdict registry and one pattern in the one file that can hold them, and it lands in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
wenzowski added a commit that referenced this pull request Sep 1, 2026
…r mechanisms

Two predicates `rules-drift` owed and did not have, and each was measured against
a claim that was live in this tree when it was written.

CLOUD-1150 §2 — `restated-arm-count-drifts`. Predicate 1 one shape up: that one
holds a restated VALUE to the mechanism, this holds a restated CLOSED COUNT over
a set of arms the mechanism owns. The cost of not having it is on the row —
`.claude/rules/toolchain.md` said "One edit is admitted, and only one" while
`shell-retirement.rego` carried three, a grooming session read the sentence,
concluded three governed programs were permanently unretirable, and wrote that
into two dispatched agent prompts and five issue bodies before anyone read the
module. **The same file was wrong again when this landed**: it said "three
shapes on its addition side" and `admitted_addition` carries four, CLOUD-1224's
`drops_a_retired_name` having arrived unmentioned. The gate caught its own file
on its first run, which is the honest way to show it discriminates.

The construction is `` `name` (N arms) `` and the authority is the modules
themselves — every rule HEAD at column zero across `policy/*.rego`, a sixth
acquisition surface for this row. A hand-written table of the arms would be the
third authority the whole gate exists to refuse. The anti-inversion bound is
predicate 1's, one shape up: prose naming a rule without asserting how many arms
it has is untouched, because this file's own rule is that a value should be read
at its authority rather than restated.

CLOUD-1206 — `schema-key-undocumented`. Predicate 3 RUN BACKWARDS: that one
refuses a named key the engine cannot emit, this refuses an emittable key the
file claiming to enumerate them does not name. Measured 2026-08-30, the schema
declared `base-delta` and `symbols` and `.claude/rules/policy-modules.md` named
neither, while that file's own closing sentence said `rules-drift` held the
lists to the schemas. A false assurance is worse than an unclaimed gap, and
`base-delta` is the projection a Todo row was about to re-derive a diff for.

SCOPED TO THE FILE THAT MAKES THE CLAIM, which is what keeps it inside the
anti-restatement bound: demanding that every prose surface enumerate 24 tree
keys would be exactly the completeness pressure predicate 1's mirror refuses. A
file that says it holds the lists in both directions has made a claim that is
present and wrong when a key is missing; a file that says nothing is an ordinary
consumer and is silent.

ONE ARM ADDED TO `named_keys`, and it is load-bearing rather than tidy. Eight of
the engine's keys carry a hyphen, which is not a legal Rego selector, so they
are only ever written `input.tree["git-history"]` — and `policy-input-key`'s
`\.` cannot match a `[`. Left unread this was a hole in BOTH directions at once:
predicate 3 could not judge a subscripted key the engine cannot emit, and this
predicate's first run over the real tree would have been eight false findings,
which is a gate nobody keeps.

Both anchors are line-oriented over prose `prettier` owns, so
`the_two_anchors_this_gate_keys_on_are_still_one_line_in_the_committed_files`
asserts over the committed files that a reflow has not pushed either across a
line break. A dead gate and a clean tree are byte-identical on the decision
surface; that case is the one thing standing between this gate and its own
opening paragraph.

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-1150, CLOUD-1206, CLOUD-506, CLOUD-770, CLOUD-932, CLOUD-845, CLOUD-1224

Admits: a44af27cfa26d1a56eefb053a30f786948d6396a021b2d2c86c1c1b1cdf04b12
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 34ec6f3
Admits-epoch: bdbfe1b93de75bd70e33efc459d9b94cc7d67e6ef40f3573c283b01014d31546
Admits-author: alec@wenzowski.com
Admits-prev: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-answer-lost: Both predicates go dead in the worst way — loading clean and deciding nothing, which is the class `.claude/rules/policy-modules.md` opens with. Without the `policy/*.rego` line source the arm-count predicate reads an empty head set and can never fire; without the pattern rows every `regex.find_n` resolves to undefined; without the verdict rows the module does not load at all, taking the four existing predicates with it. Concretely: `.claude/rules/toolchain.md` keeps saying `admitted_addition` has three arms while the module carries four — the same shape of false premise that a grooming session already wrote into two dispatched agent prompts and five issue bodies at "one" versus three — and `.claude/rules/policy-modules.md` keeps promising a both-directions guarantee it does not have.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-1150 §2 and CLOUD-1206 add two predicates to the registered `rules-drift` module, and a predicate is unreachable without three declarations that live in exactly one place by construction: the row's `line_sources` must name `policy/*.rego` or the engine hands the module no rego lines and `arm_count` is undefined; four `[[pattern]]` rows must exist or the module's `data.batten.patterns[...]` reads resolve to undefined, since an inline regex is refused at load; and two `[[verdict]]` rows must exist or the module raising an undeclared token fails to load outright, in both directions. The fact satisfying the precondition: the write is one glob added to an existing row plus four pattern rows and two verdict rows, in the one file that can hold them, landing in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
Admits: 9db820b57d821f1ebb1914a1913ee66c49ffe7a31c522b3400e917a39dd60a8e
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/rules-drift.rego
Admits-head: 34ec6f3
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Both predicates, and the live defects each was measured against. CLOUD-1150 §2: `.claude/rules/toolchain.md` says how many arms `admitted_addition` has and has been wrong twice — measured once at "one" when the module carried three, a claim a grooming session then wrote into two dispatched agent prompts and five issue bodies, and again at "three" when the module carries four. CLOUD-1206: `.claude/rules/policy-modules.md` promises in its own words that the key lists are held to the generated schemas, and only one direction was ever checked — measured 2026-08-30 the schema declared `base-delta` and `symbols` and the file named neither, with `base-delta` the projection a Todo row was about to re-derive a diff for. Neither predicate is expressible outside this module.
Admits-answer-precondition: policy/rules-drift.rego IS the surface this class names, and there is nowhere else the change can be written. CLOUD-1150 §2 and CLOUD-1206 are two predicates over prose claims — a restated `(N arms)` count held to the modules' own rule heads, and a schema key the file claiming to enumerate the key set does not name — and `rules-drift` is the registered module that already owns this class ("a value a rules file restates still agrees with the mechanism that owns it"). A second module over the same class would be the third authority the gate itself exists to refuse, and a predicate cannot live anywhere but in the module its `[[rule]]` row names. The fact satisfying the precondition: the write adds two `rules contains` declarations, two `violation` arms, one extra `named_keys` arm and eight `test_` rules to the module that already decides this class, and it lands in PR #808 where the diff is in front of every reviewer and review bot; `mise run policy-test` checks it before it lands, which is the route this class's own remedy names.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because policy/rules-drift.rego IS the owning surface for a rules-drift predicate. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file; a `.rego` module has no such tool, and its own [[redirect]] row says the remedy is "change it in a pull request" with `mise run policy-test` as the check — which is exactly this: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, policy-test green at 43 bundles / 519 cases, and a compiled tier in crates/batten/tests/it/rules_drift.rs driving the committed module over the real boundary. R-RESTORE-IT does not apply: nothing here is a mistaken write to undo — restoring the file removes the two predicates, which is the defect rather than the repair.
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch 2 times, most recently from 40aafde to e50f269 Compare September 1, 2026 15:38
wenzowski added a commit that referenced this pull request Sep 1, 2026
CLOUD-1049's second half, and it revives a gate that was registered on `main`
and deciding nothing.

`Format::for_path` searches `Format::extensions()`; no variant owns `lock`. So
a `staged` path ending in `.lock` was `NotAcquired::UnknownFormat` before a byte
was read, `input.tree.staged` never resolved it, Rego read undefined as *does
not hold*, and `policy/lock-entry-complete.rego`'s violation set was empty on
every run. Its four `test_` rules passed throughout, because `with input as`
fabricates the shape the engine could not produce -- and the module's own header
already named `staged_facts.rs` as the tier that would catch it.

`format` becomes a permitted policy column, consulted ONLY where the extension
names nothing. A fallback rather than an override, which is what keeps
`facts.rs`'s "the extension is the honest default there" true: a row cannot
re-label a `.json` as TOML and then blame the file for the parse failure.
Guessing stays refused; declaring does not.

Deliberately not extended to `documents`: an unknown-extension document is
already a hard config fault at load, which is a designed answer and a louder
one. `staged` never had that check, which is where the silence landed.

Four cases, and the pairs are what make them mean anything:

* a declared format resolves a staged path the extension cannot -- byte-identical
  fixture to the could-not-look case above it, one line added to the row, so the
  COLUMN is shown doing the work rather than the file changing
* a declared format does NOT override an extension that names one -- a `.toml`
  path with `format = "json5"` declared still reads as TOML, which is the bound
  the comment claims and would otherwise be unverified
* the committed `lock-entry-complete` module refuses a partial entry over the
  compiled binary -- a checksum with nothing to fetch, the exact defect its row
  was written for
* and passes a complete one, because a newly-live gate's likeliest failure is
  refusing everything, and nothing had ever shown this predicate discriminating

`no_artifact_name_reaches_the_core` caught a first draft of the column's comment
naming a consumer's lockfile inside `crates/batten`. It was right -- the core
knows formats, and which path carries which is the consumer's `batten.toml`
(rule 1, CLOUD-772) -- so the comment names the lockfile family instead, which
is also the truer claim.

3612 cargo tests, 42 policy bundles / 461 cases, batten-check clean.

Refs: CLOUD-1049, CLOUD-1203, CLOUD-772, CLOUD-843

Admits: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 00ffe68
Admits-epoch: 21b305197781cd9462881669ad90aeec429c80b0a323cbbc6bc150443061b6e6
Admits-author: alec@wenzowski.com
Admits-prev: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-answer-lost: policy/lock-entry-complete.rego stays a dead gate: registered, loading clean, passing its own load-time tier, and enforcing nothing on any run. A partial lockfile entry -- a checksum with nothing to fetch, the exact shape mise never repairs and which shipped once already -- goes unrefused indefinitely.
Admits-answer-precondition: The change adds a `format` column to one existing [[rule]] row so a policy module can read a staged path whose extension names no format. batten.toml IS the owning surface for a rule declaration -- there is nowhere else a row's columns can be written -- and the row in question (lock-entry-complete) has been registered and deciding nothing on every run because of the missing declaration. The write is one line added to an existing block, visible in the diff of PR #808 where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml is that owning surface: a [[rule]] column cannot be declared in the module, and the engine refuses `format` on a policy row until this change permits it. R-RESTORE-IT does not apply because the write is the deliberate content of the fix rather than damage to undo -- restoring the file returns the row to deciding nothing, which is the defect.
wenzowski added a commit that referenced this pull request Sep 1, 2026
… read

CLOUD-843's member 7, and the largest single unit in the bundle: 419 lines of
bash and a 37-case suite become `policy/lock-complete.rego`, eight predicates
over `input.tree.staged` plus one over the line surface.

WHAT UNBLOCKED IT is the `format` column landed one commit back. `mise.lock`
carries an extension no `Format` owns, so `Format::for_path` refused it before a
byte was read and `input.tree.staged["mise.lock"]` never resolved. That was not
hypothetical: `policy/lock-entry-complete.rego` was registered on `main`,
loading clean, passing its own load-time tier and enforcing nothing on every run
for exactly that reason.

THE PARSED NODE REPLACES 80 LINES OF `awk`. The predecessor read
`[[tools.<name>]]` and `[tools.<name>."platforms.<p>"]` with a hand-rolled
record stream and a second pass over `mise.toml`'s `[tools]` table; both are a
map lookup here, and the quoted-versus-bare tool name that needed a `gsub` is
just a key.

AND ONE MODULE IS SUBSUMED RATHER THAN LEFT BESIDE IT.
`policy/lock-entry-complete.rego` decided a strict subset of predicate 2 — a
platform block with a checksum and nothing to fetch — and two rows over one
question is the second authority this repository refuses everywhere else. Its
verdict token `V-LOCK-ENTRY-PARTIAL` is raised by the new module, so the
registry row is conserved rather than retired, and its two cases in
`staged_facts.rs` are repointed. That file keeps them because the ENGINE
property they assert is its own subject: that a declared `staged` path reaches a
registered row at all.

THE SPLIT BETWEEN `staged` AND `line_sources` IS DELIBERATE, and it is the one
place a verdict reads the working tree. Every lockfile and manifest decision
comes off the index, which is CLOUD-227's whole point: a cold `mise install`
rewrites `mise.lock` behind the author's back, so a gate reading the checkout
was red in every agent sandbox and green in the one CI job that runs it, for the
same commit. The workflow clause cannot use `staged` — `git::staged_facts` looks
each declared path up in the index BY NAME, so a workflow set could only reach
it as a hand-maintained inventory, and a workflow missing from that list is a
silent hole in exactly the gate that exists to close one. A glob over the line
surface has no such hole, and nothing but a person writes a workflow file.

Nine of the thirty-seven cases could not be carried verbatim and each carries
its reason on its arm. The load-bearing ones:

* `BATTEN_LOCK_PLATFORMS` is gone rather than ported — a module reads no
  environment, and the set a repository installs on is committed config rather
  than a knowable string anyone can spend to make the gate agree with them
* both exit codes are 2 now, which is the one exit table with no per-verb
  exception; the DISTINCTION survives as `V-LOCK-UNREADABLE`, its own token with
  its own remedy
* the could-not-look arm is conditioned on a staged `mise.toml` declaring
  `[tools]`, because a `[[rule]]` has no call site and an unconditional refusal
  would speak in every fixture repository inheriting this config — CLOUD-1164's
  finding avoided rather than survived

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-843, CLOUD-1203, CLOUD-223, CLOUD-227, CLOUD-281, CLOUD-333, CLOUD-593, CLOUD-1164

Admits: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bc12e3d
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-answer-lost: The whole of member 7. mise-tasks/lock-complete.sh stays alive as 419 lines of bash with a 37-case bats suite that CLOUD-843 exists to retire, and policy/lock-entry-complete.rego stays beside it as a second authority over the same question deciding a strict subset of it. Nothing else can carry the predicate: policy/lock-complete.rego is inert without a [[rule]] row naming it, and the nine V-LOCK-* verdicts are worse than inert, because a module raising a token no [[verdict]] row declares refuses the module load outright, so the module would not load at all.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-843's retirement of mise-tasks/lock-complete.sh replaces a shell program with a [[rule]] row plus nine [[verdict]] rows and one [[pattern]] row, and a rule declaration, a verdict token and a pattern id are readable by the engine from exactly one place by construction: `Rule` carries deny_unknown_fields so an undeclared column is a load error, and the verdict registry is held in BOTH directions so a module raising a token no row declares fails to load while a row nothing raises fails it too. The fact satisfying the precondition: the write is a rule row, its verdict registry and one pattern in the one file that can hold them, and it lands in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
wenzowski added a commit that referenced this pull request Sep 1, 2026
…r mechanisms

Two predicates `rules-drift` owed and did not have, and each was measured against
a claim that was live in this tree when it was written.

CLOUD-1150 §2 — `restated-arm-count-drifts`. Predicate 1 one shape up: that one
holds a restated VALUE to the mechanism, this holds a restated CLOSED COUNT over
a set of arms the mechanism owns. The cost of not having it is on the row —
`.claude/rules/toolchain.md` said "One edit is admitted, and only one" while
`shell-retirement.rego` carried three, a grooming session read the sentence,
concluded three governed programs were permanently unretirable, and wrote that
into two dispatched agent prompts and five issue bodies before anyone read the
module. **The same file was wrong again when this landed**: it said "three
shapes on its addition side" and `admitted_addition` carries four, CLOUD-1224's
`drops_a_retired_name` having arrived unmentioned. The gate caught its own file
on its first run, which is the honest way to show it discriminates.

The construction is `` `name` (N arms) `` and the authority is the modules
themselves — every rule HEAD at column zero across `policy/*.rego`, a sixth
acquisition surface for this row. A hand-written table of the arms would be the
third authority the whole gate exists to refuse. The anti-inversion bound is
predicate 1's, one shape up: prose naming a rule without asserting how many arms
it has is untouched, because this file's own rule is that a value should be read
at its authority rather than restated.

CLOUD-1206 — `schema-key-undocumented`. Predicate 3 RUN BACKWARDS: that one
refuses a named key the engine cannot emit, this refuses an emittable key the
file claiming to enumerate them does not name. Measured 2026-08-30, the schema
declared `base-delta` and `symbols` and `.claude/rules/policy-modules.md` named
neither, while that file's own closing sentence said `rules-drift` held the
lists to the schemas. A false assurance is worse than an unclaimed gap, and
`base-delta` is the projection a Todo row was about to re-derive a diff for.

SCOPED TO THE FILE THAT MAKES THE CLAIM, which is what keeps it inside the
anti-restatement bound: demanding that every prose surface enumerate 24 tree
keys would be exactly the completeness pressure predicate 1's mirror refuses. A
file that says it holds the lists in both directions has made a claim that is
present and wrong when a key is missing; a file that says nothing is an ordinary
consumer and is silent.

ONE ARM ADDED TO `named_keys`, and it is load-bearing rather than tidy. Eight of
the engine's keys carry a hyphen, which is not a legal Rego selector, so they
are only ever written `input.tree["git-history"]` — and `policy-input-key`'s
`\.` cannot match a `[`. Left unread this was a hole in BOTH directions at once:
predicate 3 could not judge a subscripted key the engine cannot emit, and this
predicate's first run over the real tree would have been eight false findings,
which is a gate nobody keeps.

Both anchors are line-oriented over prose `prettier` owns, so
`the_two_anchors_this_gate_keys_on_are_still_one_line_in_the_committed_files`
asserts over the committed files that a reflow has not pushed either across a
line break. A dead gate and a clean tree are byte-identical on the decision
surface; that case is the one thing standing between this gate and its own
opening paragraph.

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-1150, CLOUD-1206, CLOUD-506, CLOUD-770, CLOUD-932, CLOUD-845, CLOUD-1224

Admits: a44af27cfa26d1a56eefb053a30f786948d6396a021b2d2c86c1c1b1cdf04b12
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 34ec6f3
Admits-epoch: bdbfe1b93de75bd70e33efc459d9b94cc7d67e6ef40f3573c283b01014d31546
Admits-author: alec@wenzowski.com
Admits-prev: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-answer-lost: Both predicates go dead in the worst way — loading clean and deciding nothing, which is the class `.claude/rules/policy-modules.md` opens with. Without the `policy/*.rego` line source the arm-count predicate reads an empty head set and can never fire; without the pattern rows every `regex.find_n` resolves to undefined; without the verdict rows the module does not load at all, taking the four existing predicates with it. Concretely: `.claude/rules/toolchain.md` keeps saying `admitted_addition` has three arms while the module carries four — the same shape of false premise that a grooming session already wrote into two dispatched agent prompts and five issue bodies at "one" versus three — and `.claude/rules/policy-modules.md` keeps promising a both-directions guarantee it does not have.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-1150 §2 and CLOUD-1206 add two predicates to the registered `rules-drift` module, and a predicate is unreachable without three declarations that live in exactly one place by construction: the row's `line_sources` must name `policy/*.rego` or the engine hands the module no rego lines and `arm_count` is undefined; four `[[pattern]]` rows must exist or the module's `data.batten.patterns[...]` reads resolve to undefined, since an inline regex is refused at load; and two `[[verdict]]` rows must exist or the module raising an undeclared token fails to load outright, in both directions. The fact satisfying the precondition: the write is one glob added to an existing row plus four pattern rows and two verdict rows, in the one file that can hold them, landing in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
Admits: 9db820b57d821f1ebb1914a1913ee66c49ffe7a31c522b3400e917a39dd60a8e
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/rules-drift.rego
Admits-head: 34ec6f3
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Both predicates, and the live defects each was measured against. CLOUD-1150 §2: `.claude/rules/toolchain.md` says how many arms `admitted_addition` has and has been wrong twice — measured once at "one" when the module carried three, a claim a grooming session then wrote into two dispatched agent prompts and five issue bodies, and again at "three" when the module carries four. CLOUD-1206: `.claude/rules/policy-modules.md` promises in its own words that the key lists are held to the generated schemas, and only one direction was ever checked — measured 2026-08-30 the schema declared `base-delta` and `symbols` and the file named neither, with `base-delta` the projection a Todo row was about to re-derive a diff for. Neither predicate is expressible outside this module.
Admits-answer-precondition: policy/rules-drift.rego IS the surface this class names, and there is nowhere else the change can be written. CLOUD-1150 §2 and CLOUD-1206 are two predicates over prose claims — a restated `(N arms)` count held to the modules' own rule heads, and a schema key the file claiming to enumerate the key set does not name — and `rules-drift` is the registered module that already owns this class ("a value a rules file restates still agrees with the mechanism that owns it"). A second module over the same class would be the third authority the gate itself exists to refuse, and a predicate cannot live anywhere but in the module its `[[rule]]` row names. The fact satisfying the precondition: the write adds two `rules contains` declarations, two `violation` arms, one extra `named_keys` arm and eight `test_` rules to the module that already decides this class, and it lands in PR #808 where the diff is in front of every reviewer and review bot; `mise run policy-test` checks it before it lands, which is the route this class's own remedy names.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because policy/rules-drift.rego IS the owning surface for a rules-drift predicate. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file; a `.rego` module has no such tool, and its own [[redirect]] row says the remedy is "change it in a pull request" with `mise run policy-test` as the check — which is exactly this: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, policy-test green at 43 bundles / 519 cases, and a compiled tier in crates/batten/tests/it/rules_drift.rs driving the committed module over the real boundary. R-RESTORE-IT does not apply: nothing here is a mistaken write to undo — restoring the file removes the two predicates, which is the defect rather than the repair.
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch from e50f269 to 3e25468 Compare September 1, 2026 15:41
CLOUD-1049 and CLOUD-1276, which were filed as two defects on two surfaces and
turned out to be one guard.

`policy_rule` built the tree document, then discarded it whenever anything
failed to acquire:

    if !not_acquired.is_empty() {
        return Some(NotObserved::RuleSkipped);
    }

`tree_document` had already populated `input.tree.missing` correctly. Nothing
read it. So a module's could-not-look clause could not fire -- and because the
return was unconditional, neither could any OTHER predicate in the same module,
including one whose body is `true`. A gate switched off by the state of one of
its own inputs, at exit 0, with no call site left to notice.

Measured over the compiled binary, one tree and one module, varying only the
declared source: valid fires at exit 2; present-and-unparseable and
absent-via-`documents` were both SILENT. An unconditional probe arm is what
separates "the channel is empty" from "the module never ran" -- two earlier
measurements on CLOUD-1049 used a probe reading only `missing` and so reported
the smaller half.

The guard now sits BELOW `deny` and fires only when the module said nothing.
A module carrying the clause speaks; one carrying no clause still abstains, so
CLOUD-251's "never an empty deny set" is intact and `RuleSkipped` remains the
engine's own record of the abstention.

One guard closed both rows because neither was ever about acquisition:
CLOUD-1049 was a `documents` path that would not parse and CLOUD-1276 a
`line_sources` glob whose file would not decode, and the projection was right
in both cases. That is why the `.lock` measurement pointed downstream of the
push rather than at the acquisition site.

Three cases asserting the defect as measured-not-desired are inverted, each
carrying out the instruction it was written with:

* `memories.rs` -- a non-UTF-8 referrer is could-not-look, pointing at the file
* `rules_drift.rs` -- an unparseable authority is refused, beside the absent one
* `staged_facts.rs` -- an unknown extension reports instead of passing silently

The two causes stay distinct and reach one class by two routes, which is what
proves `Absent` and `Unparsed` survive the projection.

`an_unparseable_authority_no_prose_claims_against_is_still_silent` is new and is
the mirror the fix needs rather than count-padding for `assertions-not-gutted`:
now that unreadable sources reach modules, the danger inverts to a row refusing
in every tree missing one of its authorities -- the fixture-wide noise
`tree-clean` was backed out for. Without it the refusal case is satisfied by a
rule that refuses unconditionally.

`.claude/rules/policy-modules.md` has now been wrong about this channel in both
directions, and the corrected section keeps that history: it said the engine did
not populate, then that the row had shipped when it had not, then that the
channel was dead. Every revision was written confidently.

3608 cargo tests, 42 policy bundles / 461 cases, batten-check clean.

Refs: CLOUD-1049, CLOUD-1276, CLOUD-251, CLOUD-843
CLOUD-1049's second half, and it revives a gate that was registered on `main`
and deciding nothing.

`Format::for_path` searches `Format::extensions()`; no variant owns `lock`. So
a `staged` path ending in `.lock` was `NotAcquired::UnknownFormat` before a byte
was read, `input.tree.staged` never resolved it, Rego read undefined as *does
not hold*, and `policy/lock-entry-complete.rego`'s violation set was empty on
every run. Its four `test_` rules passed throughout, because `with input as`
fabricates the shape the engine could not produce -- and the module's own header
already named `staged_facts.rs` as the tier that would catch it.

`format` becomes a permitted policy column, consulted ONLY where the extension
names nothing. A fallback rather than an override, which is what keeps
`facts.rs`'s "the extension is the honest default there" true: a row cannot
re-label a `.json` as TOML and then blame the file for the parse failure.
Guessing stays refused; declaring does not.

Deliberately not extended to `documents`: an unknown-extension document is
already a hard config fault at load, which is a designed answer and a louder
one. `staged` never had that check, which is where the silence landed.

Four cases, and the pairs are what make them mean anything:

* a declared format resolves a staged path the extension cannot -- byte-identical
  fixture to the could-not-look case above it, one line added to the row, so the
  COLUMN is shown doing the work rather than the file changing
* a declared format does NOT override an extension that names one -- a `.toml`
  path with `format = "json5"` declared still reads as TOML, which is the bound
  the comment claims and would otherwise be unverified
* the committed `lock-entry-complete` module refuses a partial entry over the
  compiled binary -- a checksum with nothing to fetch, the exact defect its row
  was written for
* and passes a complete one, because a newly-live gate's likeliest failure is
  refusing everything, and nothing had ever shown this predicate discriminating

`no_artifact_name_reaches_the_core` caught a first draft of the column's comment
naming a consumer's lockfile inside `crates/batten`. It was right -- the core
knows formats, and which path carries which is the consumer's `batten.toml`
(rule 1, CLOUD-772) -- so the comment names the lockfile family instead, which
is also the truer claim.

3612 cargo tests, 42 policy bundles / 461 cases, batten-check clean.

Refs: CLOUD-1049, CLOUD-1203, CLOUD-772, CLOUD-843

Admits: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 00ffe68
Admits-epoch: 21b305197781cd9462881669ad90aeec429c80b0a323cbbc6bc150443061b6e6
Admits-author: alec@wenzowski.com
Admits-prev: 91350daa85509d1e63559cfc5d9988d121da65f8ed41b764aa7986f5a5477835
Admits-answer-lost: policy/lock-entry-complete.rego stays a dead gate: registered, loading clean, passing its own load-time tier, and enforcing nothing on any run. A partial lockfile entry -- a checksum with nothing to fetch, the exact shape mise never repairs and which shipped once already -- goes unrefused indefinitely.
Admits-answer-precondition: The change adds a `format` column to one existing [[rule]] row so a policy module can read a staged path whose extension names no format. batten.toml IS the owning surface for a rule declaration -- there is nowhere else a row's columns can be written -- and the row in question (lock-entry-complete) has been registered and deciding nothing on every run because of the missing declaration. The write is one line added to an existing block, visible in the diff of PR #808 where reviewers read it.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml is that owning surface: a [[rule]] column cannot be declared in the module, and the engine refuses `format` on a policy row until this change permits it. R-RESTORE-IT does not apply because the write is the deliberate content of the fix rather than damage to undo -- restoring the file returns the row to deciding nothing, which is the defect.
CLOUD-1218. The ratchet wedged this container: five consecutive `land` laps
refused and one 12GB cold rebuild spent, recovered only by deleting a journal
no message names.

`observed_mb = spent.min(free_mb)` (CLOUD-1241) keeps a floor from exceeding
what its own lap left free. That makes the floor a KNIFE EDGE, because
`clears_the_floor` is `free >= floor`: once the cap bites, the floor sits at
exactly the free space one lap left, and any later lap leaving a megabyte less
can never clear it. No reclaim helps -- the reclaim is already what produced the
reading. Measured here: floor 14431MB, later laps reclaimed to 14336MB.

THE DISCRIMINATOR IS THE CAP, and the landed suite already said so in its own
words -- "the SPEND and the FLOOR are two different facts once the cap bites".
So `Observed` carries `capped`, and the two kinds of number behave differently:

* uncapped is a COST -- what a lap of this shape costs. A fact about the work,
  true however the volume changes, and it ratchets exactly as before.
* capped is a CAPACITY reading -- what the volume had over. A fact about that
  moment, and a monotone high-water mark over it is a claim about a world that
  may no longer exist. It is re-measured downward by a leaner close.

Two candidate fixes were tried first and each is refuted by a landed test, which
is why neither is here: recomputing the basis from the tree at close fails
`the_consumption_observation_still_belongs_to_the_basis_the_lap_opened_under`
(whose header says collapsing the two readings "passes both cases above and
fails only here"), and discarding an observed floor whenever only the learned
number refuses guts
`the_ratchet_raises_the_floor_and_the_refusal_names_the_lap_that_set_it`, which
exists to require that refusal. Both stay green here because a cost observation
is never lowered.

The wedge ships as a test that was RED before the fix, reproducing the container
reading in miniature -- 6800MB free against a capped 7000MB floor with nothing
left to reclaim -- beside the mirror that keeps the repair from degenerating
into "never refuse".

The lowering is reported. A floor that moves under a reader without saying so is
the wedge's own failure mode wearing the opposite sign.

`JOURNAL_GENERATION` moves to `2026-09-01.capped-is-capacity`, which is also the
repair for clones already wedged: their observations carry a number whose kind
cannot be recovered, and discarding returns them to the declared floors. That
automates the manual recovery this session had to find by reading the source.
`an_observed_floor_names_the_file_that_holds_it` went red on the bump exactly as
its own comment predicted -- "when that constant next moves this case reds,
loudly, which is the right failure" -- and its stamp is updated rather than its
assertion.

3614 cargo tests.

Refs: CLOUD-1218, CLOUD-1241, CLOUD-861, CLOUD-843
…rdict

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

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

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

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

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

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

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

Refs: CLOUD-843, CLOUD-1163, CLOUD-97
… read

CLOUD-843's member 7, and the largest single unit in the bundle: 419 lines of
bash and a 37-case suite become `policy/lock-complete.rego`, eight predicates
over `input.tree.staged` plus one over the line surface.

WHAT UNBLOCKED IT is the `format` column landed one commit back. `mise.lock`
carries an extension no `Format` owns, so `Format::for_path` refused it before a
byte was read and `input.tree.staged["mise.lock"]` never resolved. That was not
hypothetical: `policy/lock-entry-complete.rego` was registered on `main`,
loading clean, passing its own load-time tier and enforcing nothing on every run
for exactly that reason.

THE PARSED NODE REPLACES 80 LINES OF `awk`. The predecessor read
`[[tools.<name>]]` and `[tools.<name>."platforms.<p>"]` with a hand-rolled
record stream and a second pass over `mise.toml`'s `[tools]` table; both are a
map lookup here, and the quoted-versus-bare tool name that needed a `gsub` is
just a key.

AND ONE MODULE IS SUBSUMED RATHER THAN LEFT BESIDE IT.
`policy/lock-entry-complete.rego` decided a strict subset of predicate 2 — a
platform block with a checksum and nothing to fetch — and two rows over one
question is the second authority this repository refuses everywhere else. Its
verdict token `V-LOCK-ENTRY-PARTIAL` is raised by the new module, so the
registry row is conserved rather than retired, and its two cases in
`staged_facts.rs` are repointed. That file keeps them because the ENGINE
property they assert is its own subject: that a declared `staged` path reaches a
registered row at all.

THE SPLIT BETWEEN `staged` AND `line_sources` IS DELIBERATE, and it is the one
place a verdict reads the working tree. Every lockfile and manifest decision
comes off the index, which is CLOUD-227's whole point: a cold `mise install`
rewrites `mise.lock` behind the author's back, so a gate reading the checkout
was red in every agent sandbox and green in the one CI job that runs it, for the
same commit. The workflow clause cannot use `staged` — `git::staged_facts` looks
each declared path up in the index BY NAME, so a workflow set could only reach
it as a hand-maintained inventory, and a workflow missing from that list is a
silent hole in exactly the gate that exists to close one. A glob over the line
surface has no such hole, and nothing but a person writes a workflow file.

Nine of the thirty-seven cases could not be carried verbatim and each carries
its reason on its arm. The load-bearing ones:

* `BATTEN_LOCK_PLATFORMS` is gone rather than ported — a module reads no
  environment, and the set a repository installs on is committed config rather
  than a knowable string anyone can spend to make the gate agree with them
* both exit codes are 2 now, which is the one exit table with no per-verb
  exception; the DISTINCTION survives as `V-LOCK-UNREADABLE`, its own token with
  its own remedy
* the could-not-look arm is conditioned on a staged `mise.toml` declaring
  `[tools]`, because a `[[rule]]` has no call site and an unconditional refusal
  would speak in every fixture repository inheriting this config — CLOUD-1164's
  finding avoided rather than survived

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-843, CLOUD-1203, CLOUD-223, CLOUD-227, CLOUD-281, CLOUD-333, CLOUD-593, CLOUD-1164

Admits: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: bc12e3d
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: 47ad3a6470e6838d0047051d67245713d475ff25881f9701d40d74e35bf957e7
Admits-answer-lost: The whole of member 7. mise-tasks/lock-complete.sh stays alive as 419 lines of bash with a 37-case bats suite that CLOUD-843 exists to retire, and policy/lock-entry-complete.rego stays beside it as a second authority over the same question deciding a strict subset of it. Nothing else can carry the predicate: policy/lock-complete.rego is inert without a [[rule]] row naming it, and the nine V-LOCK-* verdicts are worse than inert, because a module raising a token no [[verdict]] row declares refuses the module load outright, so the module would not load at all.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-843's retirement of mise-tasks/lock-complete.sh replaces a shell program with a [[rule]] row plus nine [[verdict]] rows and one [[pattern]] row, and a rule declaration, a verdict token and a pattern id are readable by the engine from exactly one place by construction: `Rule` carries deny_unknown_fields so an undeclared column is a load error, and the verdict registry is held in BOTH directions so a module raising a token no row declares fails to load while a row nothing raises fails it too. The fact satisfying the precondition: the write is a rule row, its verdict registry and one pattern in the one file that can hold them, and it lands in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
…r mechanisms

Two predicates `rules-drift` owed and did not have, and each was measured against
a claim that was live in this tree when it was written.

CLOUD-1150 §2 — `restated-arm-count-drifts`. Predicate 1 one shape up: that one
holds a restated VALUE to the mechanism, this holds a restated CLOSED COUNT over
a set of arms the mechanism owns. The cost of not having it is on the row —
`.claude/rules/toolchain.md` said "One edit is admitted, and only one" while
`shell-retirement.rego` carried three, a grooming session read the sentence,
concluded three governed programs were permanently unretirable, and wrote that
into two dispatched agent prompts and five issue bodies before anyone read the
module. **The same file was wrong again when this landed**: it said "three
shapes on its addition side" and `admitted_addition` carries four, CLOUD-1224's
`drops_a_retired_name` having arrived unmentioned. The gate caught its own file
on its first run, which is the honest way to show it discriminates.

The construction is `` `name` (N arms) `` and the authority is the modules
themselves — every rule HEAD at column zero across `policy/*.rego`, a sixth
acquisition surface for this row. A hand-written table of the arms would be the
third authority the whole gate exists to refuse. The anti-inversion bound is
predicate 1's, one shape up: prose naming a rule without asserting how many arms
it has is untouched, because this file's own rule is that a value should be read
at its authority rather than restated.

CLOUD-1206 — `schema-key-undocumented`. Predicate 3 RUN BACKWARDS: that one
refuses a named key the engine cannot emit, this refuses an emittable key the
file claiming to enumerate them does not name. Measured 2026-08-30, the schema
declared `base-delta` and `symbols` and `.claude/rules/policy-modules.md` named
neither, while that file's own closing sentence said `rules-drift` held the
lists to the schemas. A false assurance is worse than an unclaimed gap, and
`base-delta` is the projection a Todo row was about to re-derive a diff for.

SCOPED TO THE FILE THAT MAKES THE CLAIM, which is what keeps it inside the
anti-restatement bound: demanding that every prose surface enumerate 24 tree
keys would be exactly the completeness pressure predicate 1's mirror refuses. A
file that says it holds the lists in both directions has made a claim that is
present and wrong when a key is missing; a file that says nothing is an ordinary
consumer and is silent.

ONE ARM ADDED TO `named_keys`, and it is load-bearing rather than tidy. Eight of
the engine's keys carry a hyphen, which is not a legal Rego selector, so they
are only ever written `input.tree["git-history"]` — and `policy-input-key`'s
`\.` cannot match a `[`. Left unread this was a hole in BOTH directions at once:
predicate 3 could not judge a subscripted key the engine cannot emit, and this
predicate's first run over the real tree would have been eight false findings,
which is a gate nobody keeps.

Both anchors are line-oriented over prose `prettier` owns, so
`the_two_anchors_this_gate_keys_on_are_still_one_line_in_the_committed_files`
asserts over the committed files that a reflow has not pushed either across a
line break. A dead gate and a clean tree are byte-identical on the decision
surface; that case is the one thing standing between this gate and its own
opening paragraph.

3664 cargo tests, 43 policy bundles / 519 cases, batten-check clean.

Refs: CLOUD-1150, CLOUD-1206, CLOUD-506, CLOUD-770, CLOUD-932, CLOUD-845, CLOUD-1224

Admits: a44af27cfa26d1a56eefb053a30f786948d6396a021b2d2c86c1c1b1cdf04b12
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 34ec6f3
Admits-epoch: bdbfe1b93de75bd70e33efc459d9b94cc7d67e6ef40f3573c283b01014d31546
Admits-author: alec@wenzowski.com
Admits-prev: 0e9ebcb34dfdcfbfb7ebacb40597431f9bec8476f9da8d64932d80f0b645cb16
Admits-answer-lost: Both predicates go dead in the worst way — loading clean and deciding nothing, which is the class `.claude/rules/policy-modules.md` opens with. Without the `policy/*.rego` line source the arm-count predicate reads an empty head set and can never fire; without the pattern rows every `regex.find_n` resolves to undefined; without the verdict rows the module does not load at all, taking the four existing predicates with it. Concretely: `.claude/rules/toolchain.md` keeps saying `admitted_addition` has three arms while the module carries four — the same shape of false premise that a grooming session already wrote into two dispatched agent prompts and five issue bodies at "one" versus three — and `.claude/rules/policy-modules.md` keeps promising a both-directions guarantee it does not have.
Admits-answer-precondition: batten.toml IS the owning surface this class names. CLOUD-1150 §2 and CLOUD-1206 add two predicates to the registered `rules-drift` module, and a predicate is unreachable without three declarations that live in exactly one place by construction: the row's `line_sources` must name `policy/*.rego` or the engine hands the module no rego lines and `arm_count` is undefined; four `[[pattern]]` rows must exist or the module's `data.batten.patterns[...]` reads resolve to undefined, since an inline regex is refused at load; and two `[[verdict]]` rows must exist or the module raising an undeclared token fails to load outright, in both directions. The fact satisfying the precondition: the write is one glob added to an existing row plus four pattern rows and two verdict rows, in the one file that can hold them, landing in PR #808 where the diff is in front of every reviewer and review bot.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because batten.toml has no owning surface above it. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file and rewrite referrers; batten.toml has no such tool — `batten config` reads it and nothing writes it, deliberately (house-style §8: one committed authority). Its own [[redirect]] row says the remedy is "change it in a pull request", which is what this is: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, with config-lint judging the result against origin/main. R-RESTORE-IT does not apply either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
Admits: 9db820b57d821f1ebb1914a1913ee66c49ffe7a31c522b3400e917a39dd60a8e
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/rules-drift.rego
Admits-head: 34ec6f3
Admits-epoch: 9f03256285c2119bb25ca7e0aea3a55a7b354014f8e4807b4104996fd0754ea1
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Both predicates, and the live defects each was measured against. CLOUD-1150 §2: `.claude/rules/toolchain.md` says how many arms `admitted_addition` has and has been wrong twice — measured once at "one" when the module carried three, a claim a grooming session then wrote into two dispatched agent prompts and five issue bodies, and again at "three" when the module carries four. CLOUD-1206: `.claude/rules/policy-modules.md` promises in its own words that the key lists are held to the generated schemas, and only one direction was ever checked — measured 2026-08-30 the schema declared `base-delta` and `symbols` and the file named neither, with `base-delta` the projection a Todo row was about to re-derive a diff for. Neither predicate is expressible outside this module.
Admits-answer-precondition: policy/rules-drift.rego IS the surface this class names, and there is nowhere else the change can be written. CLOUD-1150 §2 and CLOUD-1206 are two predicates over prose claims — a restated `(N arms)` count held to the modules' own rule heads, and a schema key the file claiming to enumerate the key set does not name — and `rules-drift` is the registered module that already owns this class ("a value a rules file restates still agrees with the mechanism that owns it"). A second module over the same class would be the third authority the gate itself exists to refuse, and a predicate cannot live anywhere but in the module its `[[rule]]` row names. The fact satisfying the precondition: the write adds two `rules contains` declarations, two `violation` arms, one extra `named_keys` arm and eight `test_` rules to the module that already decides this class, and it lands in PR #808 where the diff is in front of every reviewer and review bot; `mise run policy-test` checks it before it lands, which is the route this class's own remedy names.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is the one I rejected, and it does not apply because policy/rules-drift.rego IS the owning surface for a rules-drift predicate. The redirect exists for .serena/memories/**, where write_memory/edit_memory/rename_memory own the file; a `.rego` module has no such tool, and its own [[redirect]] row says the remedy is "change it in a pull request" with `mise run policy-test` as the check — which is exactly this: branch claude/cloud-843-bundle-g-yvi8p9, PR #808, policy-test green at 43 bundles / 519 cases, and a compiled tier in crates/batten/tests/it/rules_drift.rs driving the committed module over the real boundary. R-RESTORE-IT does not apply: nothing here is a mistaken write to undo — restoring the file removes the two predicates, which is the defect rather than the repair.
…mine

`clippy::format_push_string` on two `push_str(&format!(..))` sites — one in
`prune.rs`'s lowering report, one in `lock_complete.rs`'s fixture builder. Both
are bound to a local now, which is the shape the neighbours in each file already
take, so each function keeps one idiom rather than two.

THE REASON THEY REACHED A LAP is worth stating: I verified with `test:cargo`,
`policy-test` and `batten-check` and never ran `lint:clippy`, so the one gate
that decides this class did not run until `land` ran the whole gate. Local
execution is free, which is exactly why the rule is to run `verify` rather than
the steps I expect to be interesting.

AND ONE FIXTURE NAME COLLIDED. `the_subscripted_spelling_counts_as_naming_a_key`
took `rules-drift-subscripted`, which
`the_subscripted_pattern_table_is_not_read_as_a_rule_name` already had; `scratch`
keys the directory on that name, so the two wrote each other's prose and whichever
ran second judged the other's tree. It passed twice and failed on the third run —
green or red by scheduling order, which is the worst way for a suite to be wrong,
and the reason the rename carries a comment rather than just a new string.

Refs: CLOUD-1218, CLOUD-843, CLOUD-1150
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch from 29ea335 to 82f59f7 Compare September 1, 2026 16:45
…akenings

The rebase took main's corpus, which costs the two suites this branch
retires and not the ones main added under it. A row naming a suite that
is gone is a cost attached to nothing, so the file is re-derived from a
`test:bats` report over the current tree rather than merged. 124 suites,
739.1s serial.

The two `Weakens:` trailers travel here because they are the branch's
declaration rather than any one commit's, and both name a smell whose
shape is syntactic while the change under it strengthens:

  rule-removed rule[lock-entry-complete]
      that row read `input.tree.staged["mise.lock"]` off a key the engine
      never inserted, so it decided nothing on every run; its successor
      `lock-complete` decides nine predicates over the same index.

  rule-predicate-changed rule[rules-drift].line_sources
      a WIDENING — `policy/*.rego` joins the sources that row already
      read, which is what lets the arm-count predicate see the modules it
      asserts about. No source is dropped.

Both are named in CLOUD-1049's Ready block as well, because a trailer
alone admits nothing where a claim receipt exists to check it against.
The clauses were written AFTER the claim rather than before it, which is
the sequencing the gate exists to prevent, and saying so here is the
whole of the disclosure — the PR body carries the same sentence.

Refs: CLOUD-843
Weakens: rule-removed rule[lock-entry-complete]
Weakens: rule-predicate-changed rule[rules-drift].line_sources
@wenzowski
wenzowski force-pushed the claude/cloud-843-bundle-g-yvi8p9 branch from 82f59f7 to 0511b04 Compare September 1, 2026 16:49
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 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 1, 2026 17:32
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

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