Skip to content

feat(policy): a fifth conserves arm for a port without a retirement - #794

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

wenzowski merged 8 commits into
mainfrom
claude/suite-subject-retirement-g2g00c

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Closes CLOUD-1268.

The class, re-derived from the tree

SubjectFacts::died is .all(): every subject a dying .bats suite declared must be absent before the deletion is admitted. Re-derived here rather than inherited —

for b in tests/*.bats; do awk 'NR==2' "$b"; done | grep -v 'mise-tasks/'

— 16 suites declare a # subject: that CLOUD-843 never retires: mise.toml, hk.pkl, batten.toml, clippy.toml, install.sh, a .claude/hooks/ program, tests/helpers.bash, Rust source. .all() can never hold for them, so those suites are undeletable by construction — 217.9s of a 1097.1s corpus, in a lane that runs --no-parallelize-within-files and whose makespan therefore cannot fall below its longest suite.

CLOUD-1156 landed the detector (policy/suite-subject-retirable.rego) and closed; its own header at :47-51 defers the recovery to "its own row", and no such row was filed. This is that row.

Why this route

The module header establishes the other two and they stay refused: re-subjecting a suite edits a governed tests/**/*.bats (V-SHELL-RULE-EDITED, one route, no bypass_env), and an in-file marker is an added line that is neither a truncation nor a repointing.

The arm

// ported: is the exact mirror of // withdrawn:. That one is admissible only where the dying file's declared subject died; this one only where it lives. Between them, both answers to "what happened to the subject" have a spelling.

It obliges more than carried, never less:

  • a target the head tree carries, exactly as the first three do;
  • plus a subject: field naming a path the dying file itself declared at base and the head tree still carries.

That field is what clears the aggregate subject-alive term — by naming the survivor, rather than by a carried row falsely claiming the case moved into helpers.bash. The subject is read from the base # subject: header rather than taken from the author, which is the anti-fabrication term.

Additive, not a loosening. named_and_alive reads carried and subsumed only and is untouched, so CLOUD-1130's refusal over the four existing markers stands byte-for-byte.

The discriminating pair is the whole commit

In ratchet.rs, over the compiled binary:

  • a_complete_port_over_a_surviving_subject_is_admitted — passes;
  • the_same_port_with_one_case_unported_still_refuses — byte-identical fixture minus one arm, and it refuses twice: the unmapped case at the suite, and the aggregate subject-alive blocker because the path is then not fully mapped.

Without the second this would be a hole and CLOUD-908's finding — "conserves files, not logic: six cases have no successor anything in the tree can name" — recurs by design.

Four more: a port whose subject died refuses (use carried); one naming no survivor refuses; one naming a survivor the suite never declared refuses; and a_carried_arm_over_a_live_subject_still_refuses is the anti-vacuity mirror that would go red if this had relaxed CLOUD-1130 rather than added to it.

Still to come on this branch

Commits 2–4 spend the spelling on pre-commit-staging (23.3s), helpers (36.1s) and session-start (135.9s) — 195.3s, 90% of the class — each deleting the suite, removing its row from suite-subject-retirable.rego's exempt map, and asserting the surviving subject's coverage in its new home. The rego and batten.toml halves of the spelling land in this PR too; both are protected paths, and their refusal's own remedy is to change them under review, which is here.

Notes for the reviewer

  • policy/shell-retirement.rego is a protected path because CLOUD-833 derives every policy row's module into the protected set — worth knowing before touching it.
  • mise-tasks/replay.sh:349 iterates carried subsumed changed, so a ported arm is skipped rather than broken; it needs no edit (and is governed, so it could not take one). The cost — ported cases get no differential replay — is real and gets its own row.
  • shell-retirement.rego's header claims rules-drift holds arm_markers to the TOML. It does not; mise-tasks/rules-drift.sh contains no reference to the arms. Nothing would have caught adding the marker in only one of the two places.

Generated by Claude Code

@linear-code

linear-code Bot commented Aug 31, 2026 •

Copy link
Copy Markdown
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 two — helpers 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.
  • **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.

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 [CLOUD-1156](https://linear.app/buttoninc/issue/CLOUD-1156/19-bats-suites-3020s-210percent-of-the-corpus-declare-a-subject-that) owns it and had already closed.
</p>
</details>
<!-- linear-review-link -->
<p><a href="https://linear.app/buttoninc/review/featpolicy-a-fifth-conserves-arm-for-a-port-without-a-retirement-12e33d433120">Review in Linear</a></p>

@coderabbitai

coderabbitai Bot commented Aug 31, 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: 0b58fa9c-c254-40ed-9859-b2474ec1af1a

📥 Commits

Reviewing files that changed from the base of the PR and between 4756ba1 and 68fb14e.

📒 Files selected for processing (10)
  • batten.toml
  • bench/suites/RESULTS.md
  • crates/batten/src/rules.rs
  • crates/batten/tests/it/bats_invocation.rs
  • crates/batten/tests/it/hk_fix_selection.rs
  • crates/batten/tests/it/ratchet.rs
  • crates/batten/tests/it/shell_retirement.rs
  • policy/suite-subject-retirable.rego
  • schema/batten.local.schema.json
  • schema/batten.schema.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • bench/suites/RESULTS.md

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


📝 Walkthrough

Walkthrough

The change adds a ported conservation arm with surviving-subject validation in Rust and Rego policy checks. It updates schemas, configuration, verdicts, and retirement documentation. It adds MCP source, credential, response-reduction, and landing-loop schema support. It migrates helper and hook-selection coverage into Rust integration tests. It also regenerates benchmark results and updates nested test-path handling.

Merge Risk: 🔵 Low · up to 68fb1

The PR adds a public MCP configuration contract for credential and source wiring; if schema-accepted settings do not match every runtime consumer, users could receive configuration that validates but does not behave as expected. The change is mergeable with explicit owner awareness of this bounded integration risk.


Note

🎁 Summarized by CodeRabbit Free

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

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

@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch from 7c04f82 to 89b4342 Compare September 1, 2026 03:09

Copy link
Copy Markdown
Contributor Author

Dispositions for the rest of the class

CLOUD-1268's Acceptance owes one per remaining member, and none may read "the subject cannot
die"
— that answer is what this PR removes. Re-derived from the tree at this head, not copied from
the row's table (CLOUD-1166's arm 4 is exactly that failure, and this row already carries one
correction for it):

for b in tests/*.bats; do printf '%s\t%s\n' "$b" "$(awk 'NR==2' "$b")"; done | grep -v 'mise-tasks/'

14 remain — 16 minus the two ported here. Not 13: the row's Acceptance said 13 while counting
session-start as ported, and its own correction later struck that suite, so it stays in the class
without being this PR's.

suite s subject disposition
session-start 97.8 .claude/hooks/session-start.sh Own row — CLOUD-1273. Struck from this PR by the row's own correction. Its cost is mise install in the two real_install_or_skip cases, not the harness, so a port moves seconds rather than removing them. Remedy is a one-line warm install in [tasks."test:bats"], and mise.toml is ungoverned so it needs no ledger arm at all.
hk-selection 2.8 hk.pkl Portable, deferred on cost. Same subject and same shape as pre-commit-staging, which this PR ports; nothing blocks it. 2.8s does not pay for a delta today.
verify 2.5 mise.toml Portable, deferred on cost.
install 1.8 install.sh Portable, deferred on cost. Its subject cannot be a policy module by construction — install.sh runs before the engine exists — but that bears on retiring install.sh, never on porting its suite.
spawn-census 0.8 clippy.toml Portable, deferred on cost.
fact-record-keying 0.7 crates/batten/src/facts.rs Portable, deferred on cost. Subject is engine source; the natural home is a sibling crates/batten/tests/*.rs.
lint-rego 0.7 mise.toml Portable, deferred on cost.
lint-deno 0.6 mise.toml Portable, deferred on cost.
commit-attribution 0.5 hk.pkl mise.toml Portable, deferred on cost. Two subjects, so a port arm names both.
commit-convention 0.3 batten.toml mise.toml Portable, deferred on cost. This is the exact suite suite-subject-retirable.rego's header names as the motivating example.
task-fail-closed 0.3 mise.toml Portable, deferred on cost.
git-hook 0.2 .claude/hooks/git-hook.sh Portable, deferred on cost.
cross-check 0.1 mise.toml Portable, deferred on cost.
zizmor-split 0.1 mise.toml Portable, deferred on cost.

Twelve of the fourteen are now portable and were not before, which is the deliverable. They total
10.6s, so each is a real delta against a small return — deferred on economics, which is a reason
that survives, rather than on impossibility, which was the old one.

Two members are deliberately still refused, and by a new arm

release-tracking-check and remedy-payload-source are not in the list above: their # subject:
headers name a workflow yaml and batten.toml, so they never appeared in the awk pass, but
CLOUD-1156 records both as gluing a governed program to an immortal subject —
mise-tasks/release-tracking-check.sh and mise-tasks/board-payloads.sh are stranded while those
suites stand.

V-PORT-SUBJECT-GOVERNED refuses a // ported: arm whose surviving subject is
governed_when_deleted, so this PR does not unstrand them, on purpose. A live governed path is
something the campaign retires; porting a suite's cases away while its governed subject stands is
the same claim named_and_alive refuses under the other four markers, decided by which word the
author typed. Each still owes its own row, exactly as CLOUD-1156 said.

Filed rather than deferred silently

  • CLOUD-1272 — replay.sh does not differentially replay a // ported: case. Its arm loop is
    for candidate in carried subsumed changed, so a ported case is skipped; the file is governed, so
    the one-line fix is unlandable and belongs to whichever row retires it.
  • CLOUD-1273 — session-start's warm-install remedy, split out by this row's own correction.

Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Correction to the dispositions comment above — the class is 17, not 14, and my stated reason was wrong

Two errors in my previous comment, both from the same cause. Correcting rather than editing, so the
mistake stays legible.

The derivation was wrong. I used:

for b in tests/*.bats; do ... done | grep -v 'mise-tasks/'

grep -v drops a line if it contains a mise-tasks/ path anywhere. Three suites declare
mixed subjects — a governed mise-tasks/ program and an immortal one on the same header line
— so the filter silently dropped them even though they are squarely in the class:

suite declared # subject:
release-tracking-check mise-tasks/release-tracking-check.sh + two .github/workflows/*.yml
remedy-payload-source mise-tasks/board-payloads.sh + batten.toml
replay mise-tasks/replay.sh + mise-tasks/replay-pointers.py

Mixed subjects are the whole reason SubjectFacts::died is .all() — they are the case the rule
exists for — and my filter was blind to exactly them. This is .claude/rules/scanning.md's own
warning landing on me: a substring pass answering a question about membership.

So the count is 17, not 14, which is also exactly what suite-subject-retirable.rego's exempt
table holds — the agreement I should have used as the check and didn't. Re-derived properly, per
subject rather than per line:

for b in tests/*.bats; do
  subj=$(awk 'NR==2' "$b" | sed 's/^# subject: *//')
  imm=""
  for s in $subj; do case "$s" in
    mise-tasks/*.py|mise-tasks/*.tsv) imm="$imm $s";;
    mise-tasks/*|tests/*.bats) ;;
    *) imm="$imm $s";;
  esac; done
  [ -n "$imm" ] && printf '%s\t%s\n' "$b" "$imm"
done

And my stated reason for their absence was wrong. I wrote that they "never appeared in the
awk pass"
because their headers "name a workflow yaml and batten.toml". They do name those —
but that is not why they were missing. They were missing because they also name a mise-tasks/
program, which is what my filter keyed on. Right conclusion, wrong mechanism, and the wrong
mechanism is the part worth correcting: it would have sent the next reader looking in the wrong
place.

What does not change

The three dispositions themselves, and the commit prose in 90238955, are unaffected — that prose
already says 13 cost + 3 structure + session-start = 17, derived from the exempt table rather than
from my filter. Restating it here so the two agree:

  • COST — 13 rows, 11.4s. Portable today, unported because the yield does not pay for a ledger
    block and a Rust port.
  • STRUCTURE — 3 rows, 9.2s. replay's .py co-subject is excluded from
    governed_when_deleted by extension; release-tracking-check and remedy-payload-source each
    name a governed and alive co-subject, which V-PORT-SUBJECT-GOVERNED refuses on purpose. This
    PR deliberately does not unstrand them — a live governed path is something the campaign retires,
    and each owes its own row, as CLOUD-1156 said.
  • session-start — 97.8s, its own row (CLOUD-1273).

13 + 3 + 1 = 17. ✅


Generated by Claude Code

@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch from 89b4342 to 9023895 Compare September 1, 2026 03:23
@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 03:56
@wenzowski
wenzowski marked this pull request as draft September 1, 2026 04:05
@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch 2 times, most recently from ad2f550 to 0ce043b Compare September 1, 2026 05:21
wenzowski added a commit that referenced this pull request Sep 1, 2026
…t arm

The case granularity landed in the previous commit; this is the half
`shell-retirement` decides. Without the marker in `arm_markers` the module
cannot SEE a `// ported:` row at all, so a deletion whose ledger maps it
completely would still raise `V-RETIREMENT-UNMAPPED` — the spelling would have
existed at one level and been invisible at the other.

Three refusals, and between them they are what make the arm oblige MORE than
the `// carried:` it sits beside rather than less:

  * `V-PORT-SUBJECT-UNNAMED` — naming the survivor is the whole arm. Without
    the field this is `carried` with a different word on it, and since the port
    arm waives the policy-surface obligation it would be strictly WEAKER than
    the marker it imitates.
  * `V-PORT-SUBJECT-RETIRED` — the mirror of `V-WITHDRAWAL-SUBJECT-ALIVE`. A
    subject that went with the suite makes this a plain retirement, and
    `carried` already spells that with one obligation fewer. Admitting both
    would let the ledger record one event in two vocabularies.
  * `V-PORT-SUBJECT-GOVERNED` — what keeps CLOUD-1130 whole in the presence of
    a fifth marker. A live governed path is something the campaign RETIRES, so
    porting a suite's cases away while its governed subject stands is exactly
    the deletion `named_and_alive` refuses under the other four — the same
    claim, decided by which word the author typed. It is also why the two
    suites stranded by a governed co-subject stay out until each has its row.

Exactly one obligation is waived and it is the one a port cannot meet: a
port-without-retirement lands nothing on the engine, so demanding a policy
surface would force the author to name a module that does not exist.
`V-SUCCESSOR-NO-TEST` is deliberately NOT waived — the compiled-binary test is
not incidental to a port, it IS the port, and a change that deleted a suite and
landed no coverage would be the defect rather than the deliverable.

The `subject:` field is marked rather than positional and excluded from
`path_successors_for`, for the reason `runs:` and `kind:` already give: nothing
positional separates the tail's fields, and a field that could satisfy
`has_binary_test` would let the survivor discharge the obligation to test it.

Five cases in `shell_retirement.rs` drive the compiled binary, which is the
tier that matters here: the arm reads a `[[pattern]]` row, and a fabricated
vocabulary would pass over a pattern the committed config does not carry — a
dead gate byte-identical to a clean tree.

`Conserves::ported`'s doc names no consumer artifact. The first draft listed
this repository's own immortal subjects and `no_artifact_name_reaches_the_core`
refused it, correctly: the engine knows only that a declared subject survived,
and which paths those are is the consumer's fact (non-negotiable rule 1).

Refs: CLOUD-1268
Admits: bc0321f2fc7cbb652a826c1d4f8928972e3d7c212a817ed94c9af37e90c74973
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 0ce043b
Admits-epoch: 96ae87916950f4f3f2ec3352c8b0c5a1b70bc206f8053658cd49b4659c82fe63
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The whole row, and the 17 suites it unblocks. `Conserves::ported` in crates/batten/src/rules.rs is inert without `ported = "// ported:"` declared here — `claimed_cases` only pushes the arm where the row declares the token — so the fifth marker would exist in the engine and be unreachable from any consumer. The three V-PORT-SUBJECT-* verdicts are worse than inert: policy/shell-retirement.rego raises them, and a token no `[[verdict]]` row declares refuses the module load outright, so the file-granularity half would not load at all. Without this write CLOUD-1268 cannot land in any form, and `SubjectFacts::died` being `.all()` leaves every suite whose declared subject survives undeletable by construction — the condition CLOUD-1156 deferred and nobody has been able to spend since.
Admits-answer-precondition: batten.toml IS the surface this class names — it is the policy authority itself, so there is no owning surface above it to route through. CLOUD-1268's deliverable is a fifth `[rule.conserves]` arm (`ported = "// ported:"`), one `[[pattern]]` row (`retirement-subject-field`) and three `[[verdict]]` rows (V-PORT-SUBJECT-UNNAMED/-RETIRED/-GOVERNED). A rule column, a pattern id and a verdict token 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. The fact satisfying the precondition: the change is four declarations in the one file that can hold them, and it lands in PR #794 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. 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, which is deliberate (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/suite-subject-retirement-g2g00c, PR #794, with `config-lint` judging the result against origin/main and a `Weakens:` trailer already carrying the one predicate change it raised. R-RESTORE-IT is not applicable either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch from 0ce043b to 3888241 Compare September 1, 2026 05:58
@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch from 3888241 to 9c035c6 Compare September 1, 2026 06:54
wenzowski added a commit that referenced this pull request Sep 1, 2026
…t arm

The case granularity landed in the previous commit; this is the half
`shell-retirement` decides. Without the marker in `arm_markers` the module
cannot SEE a `// ported:` row at all, so a deletion whose ledger maps it
completely would still raise `V-RETIREMENT-UNMAPPED` — the spelling would have
existed at one level and been invisible at the other.

Three refusals, and between them they are what make the arm oblige MORE than
the `// carried:` it sits beside rather than less:

  * `V-PORT-SUBJECT-UNNAMED` — naming the survivor is the whole arm. Without
    the field this is `carried` with a different word on it, and since the port
    arm waives the policy-surface obligation it would be strictly WEAKER than
    the marker it imitates.
  * `V-PORT-SUBJECT-RETIRED` — the mirror of `V-WITHDRAWAL-SUBJECT-ALIVE`. A
    subject that went with the suite makes this a plain retirement, and
    `carried` already spells that with one obligation fewer. Admitting both
    would let the ledger record one event in two vocabularies.
  * `V-PORT-SUBJECT-GOVERNED` — what keeps CLOUD-1130 whole in the presence of
    a fifth marker. A live governed path is something the campaign RETIRES, so
    porting a suite's cases away while its governed subject stands is exactly
    the deletion `named_and_alive` refuses under the other four — the same
    claim, decided by which word the author typed. It is also why the two
    suites stranded by a governed co-subject stay out until each has its row.

Exactly one obligation is waived and it is the one a port cannot meet: a
port-without-retirement lands nothing on the engine, so demanding a policy
surface would force the author to name a module that does not exist.
`V-SUCCESSOR-NO-TEST` is deliberately NOT waived — the compiled-binary test is
not incidental to a port, it IS the port, and a change that deleted a suite and
landed no coverage would be the defect rather than the deliverable.

The `subject:` field is marked rather than positional and excluded from
`path_successors_for`, for the reason `runs:` and `kind:` already give: nothing
positional separates the tail's fields, and a field that could satisfy
`has_binary_test` would let the survivor discharge the obligation to test it.

Five cases in `shell_retirement.rs` drive the compiled binary, which is the
tier that matters here: the arm reads a `[[pattern]]` row, and a fabricated
vocabulary would pass over a pattern the committed config does not carry — a
dead gate byte-identical to a clean tree.

`Conserves::ported`'s doc names no consumer artifact. The first draft listed
this repository's own immortal subjects and `no_artifact_name_reaches_the_core`
refused it, correctly: the engine knows only that a declared subject survived,
and which paths those are is the consumer's fact (non-negotiable rule 1).

Refs: CLOUD-1268
Admits: bc0321f2fc7cbb652a826c1d4f8928972e3d7c212a817ed94c9af37e90c74973
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 0ce043b
Admits-epoch: 96ae87916950f4f3f2ec3352c8b0c5a1b70bc206f8053658cd49b4659c82fe63
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The whole row, and the 17 suites it unblocks. `Conserves::ported` in crates/batten/src/rules.rs is inert without `ported = "// ported:"` declared here — `claimed_cases` only pushes the arm where the row declares the token — so the fifth marker would exist in the engine and be unreachable from any consumer. The three V-PORT-SUBJECT-* verdicts are worse than inert: policy/shell-retirement.rego raises them, and a token no `[[verdict]]` row declares refuses the module load outright, so the file-granularity half would not load at all. Without this write CLOUD-1268 cannot land in any form, and `SubjectFacts::died` being `.all()` leaves every suite whose declared subject survives undeletable by construction — the condition CLOUD-1156 deferred and nobody has been able to spend since.
Admits-answer-precondition: batten.toml IS the surface this class names — it is the policy authority itself, so there is no owning surface above it to route through. CLOUD-1268's deliverable is a fifth `[rule.conserves]` arm (`ported = "// ported:"`), one `[[pattern]]` row (`retirement-subject-field`) and three `[[verdict]]` rows (V-PORT-SUBJECT-UNNAMED/-RETIRED/-GOVERNED). A rule column, a pattern id and a verdict token 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. The fact satisfying the precondition: the change is four declarations in the one file that can hold them, and it lands in PR #794 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. 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, which is deliberate (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/suite-subject-retirement-g2g00c, PR #794, with `config-lint` judging the result against origin/main and a `Weakens:` trailer already carrying the one predicate change it raised. R-RESTORE-IT is not applicable 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
…t arm

The case granularity landed in the previous commit; this is the half
`shell-retirement` decides. Without the marker in `arm_markers` the module
cannot SEE a `// ported:` row at all, so a deletion whose ledger maps it
completely would still raise `V-RETIREMENT-UNMAPPED` — the spelling would have
existed at one level and been invisible at the other.

Three refusals, and between them they are what make the arm oblige MORE than
the `// carried:` it sits beside rather than less:

  * `V-PORT-SUBJECT-UNNAMED` — naming the survivor is the whole arm. Without
    the field this is `carried` with a different word on it, and since the port
    arm waives the policy-surface obligation it would be strictly WEAKER than
    the marker it imitates.
  * `V-PORT-SUBJECT-RETIRED` — the mirror of `V-WITHDRAWAL-SUBJECT-ALIVE`. A
    subject that went with the suite makes this a plain retirement, and
    `carried` already spells that with one obligation fewer. Admitting both
    would let the ledger record one event in two vocabularies.
  * `V-PORT-SUBJECT-GOVERNED` — what keeps CLOUD-1130 whole in the presence of
    a fifth marker. A live governed path is something the campaign RETIRES, so
    porting a suite's cases away while its governed subject stands is exactly
    the deletion `named_and_alive` refuses under the other four — the same
    claim, decided by which word the author typed. It is also why the two
    suites stranded by a governed co-subject stay out until each has its row.

Exactly one obligation is waived and it is the one a port cannot meet: a
port-without-retirement lands nothing on the engine, so demanding a policy
surface would force the author to name a module that does not exist.
`V-SUCCESSOR-NO-TEST` is deliberately NOT waived — the compiled-binary test is
not incidental to a port, it IS the port, and a change that deleted a suite and
landed no coverage would be the defect rather than the deliverable.

The `subject:` field is marked rather than positional and excluded from
`path_successors_for`, for the reason `runs:` and `kind:` already give: nothing
positional separates the tail's fields, and a field that could satisfy
`has_binary_test` would let the survivor discharge the obligation to test it.

Five cases in `shell_retirement.rs` drive the compiled binary, which is the
tier that matters here: the arm reads a `[[pattern]]` row, and a fabricated
vocabulary would pass over a pattern the committed config does not carry — a
dead gate byte-identical to a clean tree.

`Conserves::ported`'s doc names no consumer artifact. The first draft listed
this repository's own immortal subjects and `no_artifact_name_reaches_the_core`
refused it, correctly: the engine knows only that a declared subject survived,
and which paths those are is the consumer's fact (non-negotiable rule 1).

Refs: CLOUD-1268
Admits: bc0321f2fc7cbb652a826c1d4f8928972e3d7c212a817ed94c9af37e90c74973
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 0ce043b
Admits-epoch: 96ae87916950f4f3f2ec3352c8b0c5a1b70bc206f8053658cd49b4659c82fe63
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The whole row, and the 17 suites it unblocks. `Conserves::ported` in crates/batten/src/rules.rs is inert without `ported = "// ported:"` declared here — `claimed_cases` only pushes the arm where the row declares the token — so the fifth marker would exist in the engine and be unreachable from any consumer. The three V-PORT-SUBJECT-* verdicts are worse than inert: policy/shell-retirement.rego raises them, and a token no `[[verdict]]` row declares refuses the module load outright, so the file-granularity half would not load at all. Without this write CLOUD-1268 cannot land in any form, and `SubjectFacts::died` being `.all()` leaves every suite whose declared subject survives undeletable by construction — the condition CLOUD-1156 deferred and nobody has been able to spend since.
Admits-answer-precondition: batten.toml IS the surface this class names — it is the policy authority itself, so there is no owning surface above it to route through. CLOUD-1268's deliverable is a fifth `[rule.conserves]` arm (`ported = "// ported:"`), one `[[pattern]]` row (`retirement-subject-field`) and three `[[verdict]]` rows (V-PORT-SUBJECT-UNNAMED/-RETIRED/-GOVERNED). A rule column, a pattern id and a verdict token 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. The fact satisfying the precondition: the change is four declarations in the one file that can hold them, and it lands in PR #794 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. 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, which is deliberate (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/suite-subject-retirement-g2g00c, PR #794, with `config-lint` judging the result against origin/main and a `Weakens:` trailer already carrying the one predicate change it raised. R-RESTORE-IT is not applicable either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch 2 times, most recently from 68fb14e to 9eeda3c Compare September 1, 2026 11:46
`SubjectFacts::died` is `.all()`, so a suite whose declared `# subject:` is
not something the CLOUD-843 campaign retires can never satisfy it. Re-derived
from the tree: 16 suites declare `mise.toml`, `hk.pkl`, `batten.toml`,
`clippy.toml`, `install.sh`, a `.claude/hooks/` program, `tests/helpers.bash`
or Rust source — 217.9s of a 1097.1s corpus, undeletable by construction, in a
lane whose makespan cannot fall below its longest suite.

The two obvious routes are refused by `shell-retirement` and stay refused:
re-subjecting a suite EDITS a governed `.bats` (`V-SHELL-RULE-EDITED`, one
route, no `bypass_env`), and an in-file marker is an added line that is
neither a truncation nor a repointing. `policy/suite-subject-retirable.rego`'s
own header names the third route and defers it; this is it.

`// ported:` is the exact mirror of `// withdrawn:`. That one is admissible
only where the dying file's declared subject DIED; this one only where it
LIVES. It obliges more than `carried`, never less: a target the tree carries,
plus a `subject:` field naming a path the DYING FILE ITSELF declared at base
and the head tree still carries. The subject is read from the base header
rather than taken from the author, which is the anti-fabrication term.

That field is what clears the aggregate subject-alive term — by naming the
survivor, rather than by a `carried` row falsely claiming the case moved into
it. CLOUD-1130's refusal over the four existing markers is untouched:
`named_and_alive` reads `carried` and `subsumed` only, and this adds an
admitted shape rather than relaxing an arm.

The discriminating pair is the whole of the arm and it is in `ratchet.rs`,
over the compiled binary: a deletion with a complete port arm passes, and the
same deletion with one case unported still refuses. Without the second this
would be a hole and CLOUD-908's "conserves files, not logic" recurs by design.
`a_carried_arm_over_a_live_subject_still_refuses` is the anti-vacuity mirror
that would go red if this had relaxed CLOUD-1130 rather than added to it.

`resolved_claim` is extracted from `unconserved_cases` to stay under the line
lint; the seam is the natural one, since everything above it resolves a case
NAME and everything in it judges the claim that resolved.

Refs: CLOUD-1268
…t arm

The case granularity landed in the previous commit; this is the half
`shell-retirement` decides. Without the marker in `arm_markers` the module
cannot SEE a `// ported:` row at all, so a deletion whose ledger maps it
completely would still raise `V-RETIREMENT-UNMAPPED` — the spelling would have
existed at one level and been invisible at the other.

Three refusals, and between them they are what make the arm oblige MORE than
the `// carried:` it sits beside rather than less:

  * `V-PORT-SUBJECT-UNNAMED` — naming the survivor is the whole arm. Without
    the field this is `carried` with a different word on it, and since the port
    arm waives the policy-surface obligation it would be strictly WEAKER than
    the marker it imitates.
  * `V-PORT-SUBJECT-RETIRED` — the mirror of `V-WITHDRAWAL-SUBJECT-ALIVE`. A
    subject that went with the suite makes this a plain retirement, and
    `carried` already spells that with one obligation fewer. Admitting both
    would let the ledger record one event in two vocabularies.
  * `V-PORT-SUBJECT-GOVERNED` — what keeps CLOUD-1130 whole in the presence of
    a fifth marker. A live governed path is something the campaign RETIRES, so
    porting a suite's cases away while its governed subject stands is exactly
    the deletion `named_and_alive` refuses under the other four — the same
    claim, decided by which word the author typed. It is also why the two
    suites stranded by a governed co-subject stay out until each has its row.

Exactly one obligation is waived and it is the one a port cannot meet: a
port-without-retirement lands nothing on the engine, so demanding a policy
surface would force the author to name a module that does not exist.
`V-SUCCESSOR-NO-TEST` is deliberately NOT waived — the compiled-binary test is
not incidental to a port, it IS the port, and a change that deleted a suite and
landed no coverage would be the defect rather than the deliverable.

The `subject:` field is marked rather than positional and excluded from
`path_successors_for`, for the reason `runs:` and `kind:` already give: nothing
positional separates the tail's fields, and a field that could satisfy
`has_binary_test` would let the survivor discharge the obligation to test it.

Five cases in `shell_retirement.rs` drive the compiled binary, which is the
tier that matters here: the arm reads a `[[pattern]]` row, and a fabricated
vocabulary would pass over a pattern the committed config does not carry — a
dead gate byte-identical to a clean tree.

`Conserves::ported`'s doc names no consumer artifact. The first draft listed
this repository's own immortal subjects and `no_artifact_name_reaches_the_core`
refused it, correctly: the engine knows only that a declared subject survived,
and which paths those are is the consumer's fact (non-negotiable rule 1).

Refs: CLOUD-1268
Admits: bc0321f2fc7cbb652a826c1d4f8928972e3d7c212a817ed94c9af37e90c74973
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 0ce043b
Admits-epoch: 96ae87916950f4f3f2ec3352c8b0c5a1b70bc206f8053658cd49b4659c82fe63
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The whole row, and the 17 suites it unblocks. `Conserves::ported` in crates/batten/src/rules.rs is inert without `ported = "// ported:"` declared here — `claimed_cases` only pushes the arm where the row declares the token — so the fifth marker would exist in the engine and be unreachable from any consumer. The three V-PORT-SUBJECT-* verdicts are worse than inert: policy/shell-retirement.rego raises them, and a token no `[[verdict]]` row declares refuses the module load outright, so the file-granularity half would not load at all. Without this write CLOUD-1268 cannot land in any form, and `SubjectFacts::died` being `.all()` leaves every suite whose declared subject survives undeletable by construction — the condition CLOUD-1156 deferred and nobody has been able to spend since.
Admits-answer-precondition: batten.toml IS the surface this class names — it is the policy authority itself, so there is no owning surface above it to route through. CLOUD-1268's deliverable is a fifth `[rule.conserves]` arm (`ported = "// ported:"`), one `[[pattern]]` row (`retirement-subject-field`) and three `[[verdict]]` rows (V-PORT-SUBJECT-UNNAMED/-RETIRED/-GOVERNED). A rule column, a pattern id and a verdict token 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. The fact satisfying the precondition: the change is four declarations in the one file that can hold them, and it lands in PR #794 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. 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, which is deliberate (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/suite-subject-retirement-g2g00c, PR #794, with `config-lint` judging the result against origin/main and a `Weakens:` trailer already carrying the one predicate change it raised. R-RESTORE-IT is not applicable either — restoring the path would discard the deliverable rather than repair damage; nothing here is a mistaken write to undo.
The first two spends of CLOUD-1268's fifth ledger arm, and between them the
whole shape it exists for: a suite whose declared subject SURVIVES.

`tests/helpers.bash` is loaded by eleven other suites and `hk.pkl` is this
repository's gate definition. Neither dies; neither is touched. That is exactly
why no route existed before — `SubjectFacts::died` is `.all()`, so neither
deletion could be admitted under any of the four markers, and re-subjecting or
marking a suite in place both EDIT a governed `.bats`, which
`V-SHELL-RULE-EDITED` refuses with no override. Fifteen `// ported:` arms name
where each case went and which survivor it still accounts for.

COVERAGE DOES NOT FALL, which is the bar CLOUD-908 set and the one a port can
most easily miss:

  * `run_timeout` stands in for a GNU `timeout(1)` macOS does not ship, at nine
    call sites across three suites that assert exact numbers — so a helper
    returning merely "non-zero" would make every one pass vacuously. All eight
    branches of the exit mapping stay pinned.
  * The staging cases keep their negative control: the same fixture with
    `stash` removed shows the whole-tree fixer stamping a file the author never
    staged. If hk stops honouring the setting, that case goes green and its
    neighbour goes red — the pair saying the same thing from both sides.
  * `this_repositorys_hk_pkl_declares_stash_on_the_pre_commit_hook` reads the
    COMMITTED bytes. The other six run a fixture config, so they would all stay
    green if the real `hk.pkl` dropped the line — which is precisely how the
    setting arrived unasserted in the first place.

Both subjects are exercised rather than described: the helper is sourced from
the checkout and hk runs the fixture gate. A copy would drift and pass while the
shipped thing was broken, which is the argument `install_module` already makes
for a committed policy module.

Existing targets rather than new ones (CLOUD-1210), and the right ones:
`bats_invocation.rs` is already the file about the bats harness, and
`hk_fix_selection.rs` already has `hk.pkl` as its subject.

`policy/suite-subject-retirable.rego` loses both rows: arm C holds that table in
both directions, so an exemption outliving its suite is itself a finding.

WHAT THE REGENERATED CORPUS ACTUALLY SAYS, stated carefully because the headline
number is misleading. 138 suites -> 136, and the two removed are the two ported.
The serial total reads 1097.1s -> 667.4s, but MOST OF THAT IS NOT THIS CHANGE:
the same run puts `land-lock` at 141.8s against a recorded 195.1s and
`session-start` at 97.8s against 135.9s, neither of which this branch touches.
This change removes two suites recorded at 36.1s and 23.3s; the rest is the
machine. `session-start` still heads the table below `land-lock`, exactly as
CLOUD-1268's own correction predicted, and its remedy is CLOUD-1273's.

Refs: CLOUD-1268
…ld reason is answered

`suite-subject-retirable`'s table was written when "the subject cannot die" was
a true and sufficient reason for every row in it. It is neither any more: the
fifth ledger arm admits a deletion whose subject survives, and two rows have
already left the table by spending it.

So the prose says what an exemption MEANS now. The thirteen remaining rows total
11.4s of a 667.4s corpus, and each would owe a ledger block and a Rust port that
spawns whatever its subject is, for a sub-second yield. They are exempt on COST
— unported rather than unportable, which is the distinction the next reader
needs and the one the old wording cannot express.

`session-start` is called out separately because its reason is neither: its cost
is `mise install` in the two cases that opt back in through
`real_install_or_skip`, so a port would move the seconds rather than remove
them, and `.claude/hooks/session-start.sh` was never governed for edits or for
deletion in the first place. Its remedy is CLOUD-1273's and is not a retirement.

Refs: CLOUD-1268
`spawn_census` refused all three `#[expect(clippy::disallowed_types)]` the ports
added: every spawn annotation is an INVENTORY ROW, so its reason must open with
`stays` or `GOES`, and mine opened with the justification instead.

The verdict is `stays` in all three cases, and each now says what would retire
it rather than only why it exists: the subject of a ported suite is a bash
library or a gate definition, so exercising it means spawning, and the
annotation goes when its subject does. That is what `#[expect]` rather than
`#[allow]` buys — a deleted spawn with a stale annotation is red too, so the
inventory self-cleans in both directions.

Refs: CLOUD-1268
Weakens: rule-predicate-changed rule[bats-tests-not-deleted].conserves
The prose this corrects is mine, from two commits ago, and it over-claimed in
exactly the way CLOUD-1166 names: it said "every row remaining is exempt on
COST" over a table of SEVENTEEN rows, and gave a count of thirteen without
saying which thirteen. A reader reconciling the two would conclude the other
four are cheap ports nobody got to. That is the opposite of true for all four.

The 13/11.4s figure was right and is kept, re-derived from the regenerated
corpus rather than carried over. What is added is the boundary around it:

  * COST (13 rows, 11.4s) — portable today, unported because a sub-second yield
    does not pay for a ledger block and a Rust port.
  * STRUCTURE (3 rows, 9.2s) — `replay`'s subject is a `.py` that
    `governed_when_deleted` excludes by extension, and `release-tracking-check`
    and `remedy-payload-source` each name a GOVERNED live subject, which
    `V-PORT-SUBJECT-GOVERNED` refuses deliberately. Willingness moves none of
    them.
  * `session-start` — its own row, already argued below the table.

That third refusal is worth stating in the table's own prose rather than only
in the module that raises it: the fifth arm answers "the subject cannot die"
for a suite whose subject is ungoverned, and says nothing for a suite gluing a
live governed program to an immortal one. Reading the arm as a general answer
is how those two would get ported around instead of retired.

Refs: CLOUD-1268
CI was red on `windows` and green everywhere else, on one case:

    a_command_killed_by_a_signal_it_raised_itself_is_not_a_timeout
      left: 1   right: 143

143 is POSIX's `128 + SIGTERM`. Windows has no such encoding, so `kill -TERM $$`
under Git Bash exits 1 — and 124, 137 and 143 are every number these cases
assert, so the contract is not merely untestable there, it is meaningless.

THE DEFECT IS THE PORT'S, NOT THE CODE'S, AND IT IS A HAZARD OF PORTING RATHER
THAN OF THESE CASES. `test:bats` runs on ubuntu only, so `tests/helpers.bats`
and `tests/pre-commit-staging.bats` never faced Windows in their lives. Moving
them onto `cargo test` silently WIDENED them onto the windows matrix, which no
part of the change said out loud and no reviewer would have seen. A port moves
cases between harnesses, and two harnesses do not run the same platforms; that
difference belongs to the port to declare.

`verify` structurally could not catch it — it has no windows arm — which is what
`land` means by "a red run on a verified branch means verify and CI disagree".

So the POSIX-dependent cases carry `#[cfg(unix)]`, with the reason in the block
header rather than as a bare attribute, and stating that it was measured on CI
rather than reasoned about. The staging fixtures are gated for the same bound:
their fixer is `sed -i` in a shell step.

COVERAGE DOES NOT FALL, which is the bar this whole row is held to. These cases
ran on Linux and macOS before and still do — 3491 passed, 0 skipped, with all 32
ported cases among them. What stops is a claim about a platform their subject
was never exercised on. `this_repositorys_hk_pkl_declares_stash_on_the_pre_commit_hook`
stays ungated: reading committed bytes is portable, and it is the case that keeps
the other six from being vacuous.

Refs: CLOUD-1268
CLOUD-1210 moved `crates/batten/tests/*.rs` under `crates/batten/tests/it/`
after these arms were written. A conserves arm resolves its target by exact
tracked path, so an arm naming the pre-move location is `case-target-missing`
for every case this delta deletes — the seventeen arms of the two ported
suites. The arms elsewhere in the tree are untouched on purpose: they belong
to deltas that already landed, and only the cases a delta removes are
re-resolved.

Regenerated `bench/suites/RESULTS.md` over the rebased tree, which also
carries six suites main retired since the last run: 130 suites, 660.5s
serial.

Refs: CLOUD-1268
@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 12:19
@wenzowski
wenzowski force-pushed the claude/suite-subject-retirement-g2g00c branch from 9eeda3c to 0b31cae Compare September 1, 2026 12:19
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 0b31cae into main Sep 1, 2026
10 of 11 checks passed
@wenzowski
wenzowski deleted the claude/suite-subject-retirement-g2g00c branch September 1, 2026 12:50
wenzowski added a commit that referenced this pull request Sep 1, 2026
129 suites, 664.1s serial. `tests/session-start.bats` retires in this branch and
#794's own retirements landed on main in between, so the table records a suite
set neither side alone produces.

Refs: CLOUD-312, CLOUD-1294
wenzowski added a commit that referenced this pull request Sep 1, 2026
…that drift

PR #794 landed three `V-PORT-SUBJECT-*` verdicts and declared mutations for two
of them. `V-PORT-SUBJECT-RETIRED` had none, and the gap was two-deep: the suite
`#MUTANT-SUITE` names carried no case for that arm either. The load-time tier
pinned the predicate and `ratchet.rs` pinned the case granularity, but neither
is the declared suite, so a mutation of this arm had nothing to redden and could
only ever have been reported as a survivor.

`a_port_over_a_subject_that_died_is_refused` is that case. It discriminates
because `tests/helpers.bash` is neither a `.bats` nor under `mise-tasks/`, so
`governed_when_deleted` does not select it and its own deletion owes no arm —
leaving the dying-subject arm the only one that can fire.

Two numbers in `suite-subject-retirable.rego`'s header also came from that PR
and were wrong. The corpus denominator is DELETED rather than refreshed: it
moves every time a suite retires, so restating one is a number that goes stale
silently. `session-start`'s seconds are removed for a different reason — 97.8s
matched no measurement that exists, and the suite is the corpus's second-largest
on a cold container and unremarkable on a warm one, so the honest place for a
number is the row that owns both readings.

Both memories gain the inference their own rules did not state. A defect in a
governed program is a RETIREMENT row, never a block — "leave it alone" is the
shape for a file needing no change, not an answer to one that does. And an In
Review row is landed, not occupied, so its declared still-open acceptance
bullets are pullable work.

Refs: CLOUD-1302
Admits: d90a5f385ccca5b96d4a1196cc70273e2ec067f27aad9e66ba8c93fc13ff929a
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/shell-retirement.rego
Admits-head: 6063049
Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: V-PORT-SUBJECT-RETIRED keeps its status as the one arm of three shipped by PR #794 whose mutation was never declared, so nothing proves its compiled-binary case discriminates. That leaves a predicate that can silently stop refusing, in exactly the class CLOUD-1267 withdrew 32 exemptions to close.
Admits-answer-precondition: A registered .rego module has no owning surface that can write it: batten offers no verb that adds a #MUTANT declaration line, and the declaration must sit beside the predicate it mutates. This write is a two-line addition to policy/shell-retirement.rego landing in a reviewed PR against CLOUD-1302.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE. There is no owning surface: #MUTANT rows are read from the module source itself and batten exposes no verb that writes one. R-RESTORE-IT does not apply either, since the file is not damaged; the change is a declaration that was missing from the start.

Admits: b5eec5a0af1a244a95e0e372de28dcdb199ebb1df5ce9b7f4fa259785a8dda01
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/suite-subject-retirable.rego
Admits-head: 6063049
Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Two numbers PR #794 landed stay wrong in the tree. ':147' states a 667.4s corpus that is now 660.5s, and ':158' states session-start at 97.8s, which matches no measurement that exists — the row's cold figure is 135.9s and the warm one is 6.8s. A reader classifying the exemption table is calibrated by both.
Admits-answer-precondition: A registered .rego module has no owning surface that can write its header prose: batten offers no verb that edits a module comment. The write is a comment-only change to policy/suite-subject-retirable.rego landing in a reviewed PR against CLOUD-1302, and the predicate below it is untouched.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE. There is no owning surface for a module's header comment; the prose is read from the module source. R-RESTORE-IT does not apply — restoring the file would restore the wrong numbers, which are what this change removes.

Admits: e96e3aa4ccf27e2460fde52e56d0b5357d2e80a2ff4f775a8c8ff2c12bcfa36f
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/toolchain-and-hooks.md
Admits-head: 6063049
Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Both memories keep stating a correct rule while omitting the inference that trips over it. mem:toolchain-and-hooks names the two landable shapes for a governed file and never says which applies when you find a BUG in one — read as 'leave it alone', it makes every defect in that layer permanent. mem:workflow/board-states says In Review means merged and never says the consequence: such a row is landed, not occupied, so its open acceptance bullets are pullable. Both misreadings were measured this session.
Admits-answer-precondition: NOT the usual one, and stated plainly rather than fitted to the question. The owning surface CAN express this change and WAS used: both files were written with Serena's edit_memory, and the mediated-call gate allowed those writes. What demands this block is the commit-level half (commit.rs judge_pending), which requires an articulation for every protected path a commit touches, whether or not the write needed an override. The write is a two-paragraph addition to each memory, landing in a reviewed PR against CLOUD-1302.
Admits-answer-rejected-route: Neither route was rejected; R-USE-THE-OWNING-SURFACE was TAKEN, which is why this articulation does not fit the question. R-RESTORE-IT does not apply — nothing is damaged. The gate's question set presumes the write bypassed a surface; for .serena/memories/** written through Serena it cannot be answered as asked, and that contradiction is filed rather than papered over.

Admits: 14eb1973fd24f18fe1092e42014b0cac84beef053e935f202b0aab519cb80b01
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/workflow/board-states.md
Admits-head: 6063049
Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: mem:workflow/board-states keeps saying In Review means already merged while never drawing the consequence a reader needs: such a row is LANDED, not OCCUPIED, so its declared still-open acceptance bullets are pullable work and its attached PRs are evidence of landed work rather than of a competitor. Measured this session on CLOUD-1218, which was declined on exactly that misreading.
Admits-answer-precondition: NOT the usual one, and stated plainly rather than fitted to the question. The owning surface CAN express this change and WAS used: this file was written with Serena's edit_memory, and the mediated-call gate allowed the write. What demands this block is the commit-level half (commit.rs judge_pending), which requires an articulation for every protected path a commit touches, whether or not the write needed an override. The write adds one numbered entry to the memory's own 'three things that trip agents up' section, landing in a reviewed PR against CLOUD-1302.
Admits-answer-rejected-route: Neither route was rejected; R-USE-THE-OWNING-SURFACE was TAKEN, which is why this articulation does not fit the question. R-RESTORE-IT does not apply — nothing is damaged. The gate's question set presumes the write bypassed a surface; for .serena/memories/** written through Serena it cannot be answered as asked, and that contradiction is filed rather than papered over.
wenzowski added a commit that referenced this pull request Sep 1, 2026
…that drift

PR #794 landed three `V-PORT-SUBJECT-*` verdicts and declared mutations for two
of them. `V-PORT-SUBJECT-RETIRED` had none, and the gap was two-deep: the suite
`#MUTANT-SUITE` names carried no case for that arm either. The load-time tier
pinned the predicate and `ratchet.rs` pinned the case granularity, but neither
is the declared suite, so a mutation of this arm had nothing to redden and could
only ever have been reported as a survivor.

`a_port_over_a_subject_that_died_is_refused` is that case. It discriminates
because `tests/helpers.bash` is neither a `.bats` nor under `mise-tasks/`, so
`governed_when_deleted` does not select it and its own deletion owes no arm —
leaving the dying-subject arm the only one that can fire.

Two numbers in `suite-subject-retirable.rego`'s header also came from that PR
and were wrong. The corpus denominator is DELETED rather than refreshed: it
moves every time a suite retires, so restating one is a number that goes stale
silently. `session-start`'s seconds are removed for a different reason — 97.8s
matched no measurement that exists, and the suite is the corpus's second-largest
on a cold container and unremarkable on a warm one, so the honest place for a
number is the row that owns both readings.

Both memories gain the inference their own rules did not state. A defect in a
governed program is a RETIREMENT row, never a block — "leave it alone" is the
shape for a file needing no change, not an answer to one that does. And an In
Review row is landed, not occupied, so its declared still-open acceptance
bullets are pullable work.

Refs: CLOUD-1302
Admits: d90a5f385ccca5b96d4a1196cc70273e2ec067f27aad9e66ba8c93fc13ff929a
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/shell-retirement.rego
Admits-head: 6063049
Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: V-PORT-SUBJECT-RETIRED keeps its status as the one arm of three shipped by PR #794 whose mutation was never declared, so nothing proves its compiled-binary case discriminates. That leaves a predicate that can silently stop refusing, in exactly the class CLOUD-1267 withdrew 32 exemptions to close.
Admits-answer-precondition: A registered .rego module has no owning surface that can write it: batten offers no verb that adds a #MUTANT declaration line, and the declaration must sit beside the predicate it mutates. This write is a two-line addition to policy/shell-retirement.rego landing in a reviewed PR against CLOUD-1302.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE. There is no owning surface: #MUTANT rows are read from the module source itself and batten exposes no verb that writes one. R-RESTORE-IT does not apply either, since the file is not damaged; the change is a declaration that was missing from the start.

Admits: b5eec5a0af1a244a95e0e372de28dcdb199ebb1df5ce9b7f4fa259785a8dda01
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/suite-subject-retirable.rego
Admits-head: 6063049
Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Two numbers PR #794 landed stay wrong in the tree. ':147' states a 667.4s corpus that is now 660.5s, and ':158' states session-start at 97.8s, which matches no measurement that exists — the row's cold figure is 135.9s and the warm one is 6.8s. A reader classifying the exemption table is calibrated by both.
Admits-answer-precondition: A registered .rego module has no owning surface that can write its header prose: batten offers no verb that edits a module comment. The write is a comment-only change to policy/suite-subject-retirable.rego landing in a reviewed PR against CLOUD-1302, and the predicate below it is untouched.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE. There is no owning surface for a module's header comment; the prose is read from the module source. R-RESTORE-IT does not apply — restoring the file would restore the wrong numbers, which are what this change removes.

Admits: e96e3aa4ccf27e2460fde52e56d0b5357d2e80a2ff4f775a8c8ff2c12bcfa36f
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/toolchain-and-hooks.md
Admits-head: 6063049
Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Both memories keep stating a correct rule while omitting the inference that trips over it. mem:toolchain-and-hooks names the two landable shapes for a governed file and never says which applies when you find a BUG in one — read as 'leave it alone', it makes every defect in that layer permanent. mem:workflow/board-states says In Review means merged and never says the consequence: such a row is landed, not occupied, so its open acceptance bullets are pullable. Both misreadings were measured this session.
Admits-answer-precondition: NOT the usual one, and stated plainly rather than fitted to the question. The owning surface CAN express this change and WAS used: both files were written with Serena's edit_memory, and the mediated-call gate allowed those writes. What demands this block is the commit-level half (commit.rs judge_pending), which requires an articulation for every protected path a commit touches, whether or not the write needed an override. The write is a two-paragraph addition to each memory, landing in a reviewed PR against CLOUD-1302.
Admits-answer-rejected-route: Neither route was rejected; R-USE-THE-OWNING-SURFACE was TAKEN, which is why this articulation does not fit the question. R-RESTORE-IT does not apply — nothing is damaged. The gate's question set presumes the write bypassed a surface; for .serena/memories/** written through Serena it cannot be answered as asked, and that contradiction is filed rather than papered over.

Admits: 14eb1973fd24f18fe1092e42014b0cac84beef053e935f202b0aab519cb80b01
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: .serena/memories/workflow/board-states.md
Admits-head: 6063049
Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: mem:workflow/board-states keeps saying In Review means already merged while never drawing the consequence a reader needs: such a row is LANDED, not OCCUPIED, so its declared still-open acceptance bullets are pullable work and its attached PRs are evidence of landed work rather than of a competitor. Measured this session on CLOUD-1218, which was declined on exactly that misreading.
Admits-answer-precondition: NOT the usual one, and stated plainly rather than fitted to the question. The owning surface CAN express this change and WAS used: this file was written with Serena's edit_memory, and the mediated-call gate allowed the write. What demands this block is the commit-level half (commit.rs judge_pending), which requires an articulation for every protected path a commit touches, whether or not the write needed an override. The write adds one numbered entry to the memory's own 'three things that trip agents up' section, landing in a reviewed PR against CLOUD-1302.
Admits-answer-rejected-route: Neither route was rejected; R-USE-THE-OWNING-SURFACE was TAKEN, which is why this articulation does not fit the question. R-RESTORE-IT does not apply — nothing is damaged. The gate's question set presumes the write bypassed a surface; for .serena/memories/** written through Serena it cannot be answered as asked, and that contradiction is filed rather than papered over.
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