From 5b27cdbaa35513614ddbf5f9d2e29dd2ec1f727f Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 16:03:41 +0000 Subject: [PATCH 1/2] fix(policy): declare the third ported mutation, and drop two numbers that drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: 606304936fbd9a3ee6fe50b7938f7b39c4f02fce 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: 606304936fbd9a3ee6fe50b7938f7b39c4f02fce 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: 606304936fbd9a3ee6fe50b7938f7b39c4f02fce 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: 606304936fbd9a3ee6fe50b7938f7b39c4f02fce 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. --- .serena/memories/toolchain-and-hooks.md | 26 +++++++++++++++ .serena/memories/workflow/board-states.md | 24 ++++++++++++++ crates/batten/tests/it/shell_retirement.rs | 38 ++++++++++++++++++++++ policy/shell-retirement.rego | 1 + policy/suite-subject-retirable.rego | 25 +++++++++----- 5 files changed, 106 insertions(+), 8 deletions(-) diff --git a/.serena/memories/toolchain-and-hooks.md b/.serena/memories/toolchain-and-hooks.md index 880557ba2..2cbd26956 100644 --- a/.serena/memories/toolchain-and-hooks.md +++ b/.serena/memories/toolchain-and-hooks.md @@ -421,6 +421,32 @@ bypass. `.claude/rules/toolchain.md` is where that binds; read it there rather than here, because a second copy of the governed-path predicate is exactly the drift these notes exist to avoid (CLOUD-1132). +**AND THE CASE THAT ACTUALLY ARISES IS THE ONE NEITHER SHAPE NAMES: you find a +BUG in a governed program.** "Leave it alone" is the shape for a file that needs +no change. It is not an answer to one that does — and read as though it were, it +turns every defect in this layer into a permanent one, which is precisely +backwards, since the layer is where the defects are. A needed fix in a governed +`mise-tasks/*.sh` is a **RETIREMENT ROW**: land the predicate as a +`policy/*.rego` module plus a `crates/batten/tests/**/*.rs` tier, delete the +program and its suite with one `conserves` arm per path, and drop it from +`$MUTANT_GATES`. Never a block, and never a filed-and-deferred note that leaves +the bug standing. + +**Measured, three times in one session (PR #794).** `mise-tasks/replay.sh` and +`mise-tasks/config-lint.sh` were each reported as carrying an unlandable fix, +with "the file is governed" given as the reason — and in the `config-lint` case +the defect was real, verified, and was the thing that had just admitted that +same PR's own config smell. Each was a retirement whose §1 had been written in +the wrong shape, which is the sentence `.claude/rules/toolchain.md` already +spends a paragraph on. **The tell is reaching for the word _unlandable_ about a +path this campaign exists to retire**: the campaign IS the route, so a governed +path is the one place a fix is guaranteed to have one. + +Scoping it is usually smaller than it looks — ask what the shell actually still +owns. `config-lint`'s smell detection is already `batten config lint`; only the +admission layer (a claim receipt plus a `Weakens:` trailer) is bash, and the +engine already owns hash-bound admissions in `batten override request`/`spend`. + The `*-check` decision halves — `graph-check`, `landed-check`, `ready-lint`, `verified` — use **exit 1 = violation, exit 2 = could not read the input**. Batten's contract (house-style §7, CLOUD-226) is the exact opposite: **1 = diff --git a/.serena/memories/workflow/board-states.md b/.serena/memories/workflow/board-states.md index cffe9d881..719ca6936 100644 --- a/.serena/memories/workflow/board-states.md +++ b/.serena/memories/workflow/board-states.md @@ -159,6 +159,30 @@ performed; whether it produces the property is the question the edit does not as Symmetric with Done: the Done _gate_ ("landed on `main` by fast-forward, CI green") is a predicate; the `Done` _column_ is where issues land once it holds. +1b. **An In Review row is LANDED, NOT OCCUPIED — so its open work is pullable, +not somebody else's.** Item 2 below says In Review means already merged; this +is the inference a reader takes from it and gets backwards. A row sitting In +Review with attachments looks exactly like a row somebody is actively +working, and the attachments are the strongest-looking evidence — they are +evidence of work **already landed**, which is the opposite of a competitor. + +So a row In Review whose body declares a still-open acceptance bullet ("still +open, and the reason this row is not Done") is **available work**. Pull it: +`claim-check`, board move, branch, as for any other row. The duplicate-claim +fear the entry further down records is about the pull-time window, where a +competitor has published nothing — In Review is the state where a competitor +has published everything, which is why it cannot be that case. + +Measured 2026-09-01: CLOUD-1218 was declined as "owned, actively — racing it +is CLOUD-230's defect", citing its four attached PRs. All four had landed; +`crates/batten/src/prune.rs` was ungoverned; the row's own last bullet named +the remaining work. Nothing was blocking anything. + +The neighbouring rule is real and is about **content, not availability**: item +0's table says an In Review body is a RECORD, so a wrong design there is fixed +by a new row rather than a rewrite. That constrains what you may edit in the +body. It says nothing about whether you may do the work the body asks for. + 2. **In Review means already merged — this is trunk-based.** Per https://trunkbaseddevelopment.com/ we review _after_ merge, before release, to avoid large divergence on `main`. Unreviewed code paths are kept out of diff --git a/crates/batten/tests/it/shell_retirement.rs b/crates/batten/tests/it/shell_retirement.rs index acea3a6cc..9fde6d805 100644 --- a/crates/batten/tests/it/shell_retirement.rs +++ b/crates/batten/tests/it/shell_retirement.rs @@ -339,6 +339,44 @@ fn a_port_naming_a_live_governed_subject_is_refused() { ); } +/// THE THIRD `ported` REFUSAL, and the one that shipped with no mutation over +/// it. A subject that DIED is not a surviving one: that is a plain retirement, +/// which `carried` already spells with less, and admitting the same event under +/// both markers lets the ledger record it in two vocabularies. +/// +/// Its absence from THIS file is what the missing `#MUTANT` row was hiding, and +/// the gap was two-deep rather than one. The load-time tier pinned the predicate +/// and `ratchet.rs` pinned the case granularity, but neither is the suite +/// `#MUTANT-SUITE` names — so a mutation of this arm had nothing here to redden +/// and could only ever have been declared as a survivor (CLOUD-1302). +/// +/// `tests/helpers.bash` is deliberately the dying subject. It is neither a +/// `.bats` nor under `mise-tasks/`, so `governed_when_deleted` does not select +/// it and its own deletion owes no arm — which leaves this arm the only one that +/// can fire, and is what makes the mutation discriminate rather than survive +/// behind a conjunct some other arm already excludes. +#[test] +fn a_port_over_a_subject_that_died_is_refused() { + let root = repo( + "ported-subject-died", + &[ + ("tests/old-gate.bats", IMMORTAL_SUITE), + ("tests/helpers.bash", "helpers\n"), + ], + &Head { + written: &[( + "crates/batten/tests/old_gate.rs", + &ledger_ported("tests/old-gate.bats", "tests/helpers.bash"), + )], + removed: &["tests/old-gate.bats", "tests/helpers.bash"], + }, + ); + assert!( + !findings(&root).is_empty(), + "a port whose subject died is a retirement, and `carried` is its spelling" + ); +} + /// The truncation arm, over the engine — and it is the shape that forced the /// clause rather than one imagined for it. /// diff --git a/policy/shell-retirement.rego b/policy/shell-retirement.rego index ec79e38d5..95b6f8ee2 100644 --- a/policy/shell-retirement.rego +++ b/policy/shell-retirement.rego @@ -81,6 +81,7 @@ # withdrawn here, so the field has to name a case that exists, and both do. #MUTANT port-subject-unnamed-unchecked|s@count(ported_subjects(path)) == 0@false@|a_port_naming_no_subject_is_refused #MUTANT port-subject-governed-unchecked|s@governed_when_deleted(subject)@false@|a_port_naming_a_live_governed_subject_is_refused +#MUTANT port-subject-retired-unchecked|s@subject in {gone | some gone in delta.deleted}@false@|a_port_over_a_subject_that_died_is_refused # THE CASE EXERCISES THE INVOCATION CLAUSE, NOT THE PATH ONE. Its caller binds # `$(dirname "$0")/old-gate.sh` to a variable and repoints at `mise run # old-gate`, which is CLOUD-1219's decomposition — so a mutation of diff --git a/policy/suite-subject-retirable.rego b/policy/suite-subject-retirable.rego index 875151030..9de9b12e1 100644 --- a/policy/suite-subject-retirable.rego +++ b/policy/suite-subject-retirable.rego @@ -144,7 +144,7 @@ declared[path] := parts if { # table would conclude the other four are cheap ports nobody got to, which is the # opposite of true for every one of them. # -# * COST (13 rows, 11.4s total against a 667.4s corpus). Portable today — each +# * COST (13 rows, 11.4s total). Portable today — each # would owe a ledger block and a Rust port spawning whatever its subject is, # for a sub-second yield. Unported, NOT unportable, and a row that wants one # only has to be worth writing. @@ -155,14 +155,23 @@ declared[path] := parts if { # something the campaign retires, and porting a suite away from one is the # claim `named_and_alive` already refuses under the other four markers. No # amount of willingness moves these; each owes its own row. -# * ITS OWN ROW (`session-start`, 97.8s), below. +# * ITS OWN ROW (`session-start`), below. # -# ONE IS NOT ABOUT COST AND HAS ITS OWN ROW. `session-start` is the largest suite -# here by two orders of magnitude, and porting it would move its seconds rather -# than remove them — the cost is `mise install` in the two cases that opt back in -# through `real_install_or_skip`, not the bats harness. Its remedy is warming the -# toolchain once in `[tasks."test:bats"]`, which is CLOUD-1273's and is not a -# retirement at all. `.claude/hooks/session-start.sh` was never governed for edits +# THE SECONDS ABOVE ARE READ FROM `bench/suites/RESULTS.md` AND NOTHING HOLDS THEM +# THERE. They are stated because the classification is about relative cost, and a +# corpus DENOMINATOR is deliberately not: it moves every time a suite retires, so +# restating one here is a number that goes stale silently (CLOUD-770's rule). +# The first version of this table carried one, and it was wrong within a week. +# +# ONE IS NOT ABOUT COST AND HAS ITS OWN ROW, and its cost is the one number no +# reading of this tree settles. `session-start`'s seconds are `mise install` in +# the two cases that opt back in through `real_install_or_skip`, not the bats +# harness — so it is the corpus's second-largest suite on a COLD container and +# unremarkable on a warm one, and the generated table reports whichever the run +# that produced it had. Porting it would move those seconds rather than remove +# them. Its remedy is warming the toolchain once in `[tasks."test:bats"]`, which +# is CLOUD-1273's and is not a retirement at all; that row carries both readings +# and is where a number for this suite belongs. `.claude/hooks/session-start.sh` was never governed for edits # OR for deletion either, so filing it under an immortal subject sent every pass at # it toward a retirement it never needed. # From be5a33c1ef7ce806f200a07cb2f7265f0b9810a5 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 16:20:29 +0000 Subject: [PATCH 2/2] fix(policy): the mutation row's own expression carried the field separator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `#MUTANT` rows are pipe-delimited, and the expression I wrote to kill `V-PORT-SUBJECT-RETIRED`'s dying-subject conjunct contained a Rego set comprehension — `{gone | some gone in delta.deleted}`. The `|` inside it read as a field separator, so the declaration parsed as four fields where three are wanted and the sweep reported it `malformed-row`: could-not-look, not a pass. That is the gate discriminating rather than a nuisance. A row this shape would have sat in the file looking like coverage while never applying a mutation — the exact failure CLOUD-1302 exists to close, reproduced one level up in the fix for it. Anchored on the line prefix instead, which is how `shell-subject-alive-unchecked` already spells the same problem: replace `\tsubject in ` with `\tfalse #`, so the remainder of the line becomes a comment and no `|` appears in the expression. `^\t` keeps it off the two neighbouring `some subject in` and `not subject in` lines, which carry the same comprehension and must not move. Refs: CLOUD-1302 Admits: 72ebe3172dea00a954c21a74d6bc2a0fa6183424c25d36d415a7672367092507 Admits-rule: protected-mutation Admits-verdict: V-PROTECTED-MUTATION Admits-subject: policy/shell-retirement.rego Admits-head: 1c4f4456fad5aa121f223af8c238193967ee0db9 Admits-epoch: 71bf7f9f4f378e71c73ceeea5eb44c0217d75bdca1c242d0f9cdfb31f03c5117 Admits-author: alec@wenzowski.com Admits-prev: d90a5f385ccca5b96d4a1196cc70273e2ec067f27aad9e66ba8c93fc13ff929a Admits-answer-lost: The row stays malformed and the sweep keeps reporting 'port-subject-retired-unchecked malformed-row (4 fields, want 3)' — could-not-look, not a pass. The pipe inside the Rego comprehension {gone | some gone in delta.deleted} is read as the #MUTANT field separator, so the declaration parses as four fields and the mutation is never applied. V-PORT-SUBJECT-RETIRED would remain the one arm of three with no mutation actually exercised, which is the whole point of CLOUD-1302. Admits-answer-precondition: A registered .rego module has no owning surface that can write it: batten offers no verb that edits a #MUTANT declaration, and the row must sit beside the predicate it mutates. This is a one-line repair to the row added in 1c4f4456 on this same branch, landing in draft PR #813 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 and batten exposes no verb that writes one. R-RESTORE-IT is the wrong direction — restoring the file would restore the malformed row this repairs. --- policy/shell-retirement.rego | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policy/shell-retirement.rego b/policy/shell-retirement.rego index 95b6f8ee2..c4562e662 100644 --- a/policy/shell-retirement.rego +++ b/policy/shell-retirement.rego @@ -81,7 +81,7 @@ # withdrawn here, so the field has to name a case that exists, and both do. #MUTANT port-subject-unnamed-unchecked|s@count(ported_subjects(path)) == 0@false@|a_port_naming_no_subject_is_refused #MUTANT port-subject-governed-unchecked|s@governed_when_deleted(subject)@false@|a_port_naming_a_live_governed_subject_is_refused -#MUTANT port-subject-retired-unchecked|s@subject in {gone | some gone in delta.deleted}@false@|a_port_over_a_subject_that_died_is_refused +#MUTANT port-subject-retired-unchecked|s@^\tsubject in @\tfalse #@|a_port_over_a_subject_that_died_is_refused # THE CASE EXERCISES THE INVOCATION CLAUSE, NOT THE PATH ONE. Its caller binds # `$(dirname "$0")/old-gate.sh` to a variable and repoints at `mise run # old-gate`, which is CLOUD-1219's decomposition — so a mutation of