fix(policy): let a retirement name the DRY home, and say which home is which - #751
fix(policy): let a retirement name the DRY home, and say which home is which#751wenzowski wants to merge 3 commits into
Conversation
CLOUD-1150 `.claude/rules/toolchain.md` says one sibling-edit admission is allowed where the module carries three, and `rules-drift` cannot see a restated rule-NAME set
Why
A stale parenthetical is a typo. A stale closed count that forbids looking further is a false premise delivered with the authority of the rule. That is Measured cost, in this repository, this weekA grooming session read that sentence, concluded three governed programs were permanently unretirable, and wrote that conclusion into two dispatched agent prompts and five issue bodies (CLOUD-1100, CLOUD-761, CLOUD-1092, CLOUD-1113, CLOUD-1111). CLOUD-1100's scope was cut on it, with the row recording *"the program stays in the tree, dormant behind the compiled authority, and that is the honest state rather than a shim." *CLOUD-761's consumer half was blocked on it outright. Both dispatched sessions were running by the time it was caught and could not be recalled. The correction that followed was also wrong, in the other direction — see CLOUD-1149. Three passes over one sentence, two of them landing on the board. The mechanism half, and its coupling
"One edit is admitted, and only one" is present and wrong. It is not an absent-value case, so gating it does not invert the file's own anti-restatement discipline. The gap is only in shape:
The prose half is landing separately and immediately
Refinement — Ready (gate the restated rule-name set; retire Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found because the sentence this row is about caused two dispatched agent sessions and five issue bodies to be written on a false premise, and the gate that exists to catch exactly this could not see it. CLOUD-1152 Batten adjudicates for six harnesses and documents itself for one: every load-bearing rule lives in `.claude/rules/`, as prose, which is rule 2's own prohibition
Why
The repository's own operating doctrine lives in The root document already solved this and the fix is one line of Neutral content, vendor-named alias. Correct. Then A completion gate whose threat model is honest error, shipping as a consumer-installable product, cannot have its own rules readable by one sixth of the hosts it adjudicates for. That is not a tidiness complaint; it is the same class as non-negotiable rule 1. Rule 1 forbids consumer-specific identifiers in the core so the core stays portable. Nothing forbids vendor-specific instruction surfaces, and the result is a project whose portability claim is true of its binary and false of its doctrine. The deeper violation is rule 2, not the folder name
Measured, this week, in this repositoryCLOUD-1150: The response was to write better prose in the same folder, then file CLOUD-1150 proposing a gate to keep the vendor prose honest. That is a mechanism for maintaining the wrong artifact, and this row supersedes that framing: 1150's gate is worth having for whatever prose remains, but it is downstream of deciding what should be prose at all. This is not the first instance. CLOUD-358 (Backlog) is the same class one level down — "a rule shipped without its gate, in the file that forbids exactly that." CLOUD-994 is a third. CLOUD-1119 was a fourth. The pattern recurs because the surface invites it. The structural cause, which is why this needs a decision rather than a cleanupThere is nowhere harness-neutral to put doctrine, and that is enforced:
So the vendor folder was the only unforbidden surface with a trigger. What the answer is notNot "copy CORRECTION (2026-08-30) — "nowhere neutral" is false, and it changes the deliverableThe structural-cause section above says Every affordance the vendor folder offers has a shipped neutral equivalent:
**It reaches 1 of 6 hosts. **
The So the vendor folder did not win by elimination against nothing. It tied with the neutral channel on reach — one harness either way — and beat it on effort. That is a far more actionable diagnosis than "nowhere neutral exists", and it inverts the fix: the blocking question is the advisory emitter, not a folder. Relocating doctrine first moves it from a folder one harness reads to a channel one harness receives, and buys nothing. Closing One bound to preserve in any emitter work: The two other constraints in the structural-cause section stand and are unaffected: rule 7 still forbids The move is not a
|
| file | what it does |
|---|---|
rules_builtin_claims.rs:70 |
const RULES_DIR: &str = ".claude/rules"; — a named constant |
memory_injection.rs:84-89,148-153 |
seven path literals; asserts a per-file injection census |
hk_fix_selection.rs:58,71,232 |
writes .claude/rules/toolchain.md into a fixture and asserts line_sources names it |
ambient_authority.rs:37 |
cites .claude/rules/rust.md as a co-authority with clippy.toml |
Two of those are gates over the prose, living in the core crate — rules_builtin_claims.rs holds policy-modules.md's claims to the builtins, and scanner_taxonomy.rs asserts scanning.md still names an instrument per question class. So the repo-agnostic core contains tests asserting the CONTENT of one vendor's instruction files, which is this row's thesis one layer deeper than the folder name.
Two consequences for §1. First, crates/batten/tests/** belongs in the authority boundary — it is currently absent, and an implementer honouring §1 as written would discover the coupling mid-move with a red suite and no sanctioned file to fix it in. Second, the classification in §2 gains a fourth question per rule: does a test in the core crate assert this file's content, and does that test move, get re-pathed, or get deleted? A rule whose only mechanism is a core-crate test asserting its prose is not class (a) — the mechanism does not own the rule, it owns the sentence.
Cosmetic and worth noting only so it is not mistaken for the above: src/policy/presets/shell-hygiene/sibling-resolves.rego:178-179 uses .claude/hooks/... as test-fixture data inside a shipped preset. The rule itself hardcodes no prefix — its own comment says that is the bug it avoids — so this is a fixture-naming choice, not a rule 1 violation, and it should not be used to inflate the count above.
Refinement — Ready (decide the neutral instruction surface, then move what is prose-only)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- Authority boundary (§1). The instruction surface itself:
AGENTS.md,.claude/rules/**, whatever neutral home this row decides on, **and **crates/batten/tests/**— four of its files hard-code the vendor path, two of them as gates over the prose, so the move reds the core suite without them. See the section above. **No **mise-tasks/**program and no **tests/**/*.batsis edited or added —rules-drift.shis governed, so any change to its globs is a retirement (CLOUD-1150's coupling), not an edit, and this row does not assume otherwise. - Computable predicate (§2), and it is the row's real deliverable. Each rule currently in
.claude/rules/**is classified into exactly one of three: (a) mechanism-owned — the authority is a module, task header or source comment, and the prose becomes a pointer; (b) genuinely vendor-specific — Claude Code's frontmatter triggers,.claude/settings.jsonhook wiring — and stays; (c) harness-neutral doctrine with no mechanism — which is rule 2's half a change, and each one either gets a gate or is recorded as a known gap. The classification is the artefact; the moves follow from it. - The blocking question is the ADVISORY EMITTER, not a folder (§2). Per the correction above, the neutral channel exists and reaches 1 of 6 hosts because CLOUD-44 landed the decision emitter and not the advisory one. Close
GeminiClifirst — the one host declaredYeswith an emptydelivered_on, so a pure Batten gap and 1/6 → 2/6 with a single shim. If that work is larger than this row, split it out and make this rowblockedByit; do not relocate prose ahead of the channel meant to carry it. - The neutral home is the FALLBACK, not the blocking question (§2). A root
rules/directory is permitted (no-docs-treeboundsdocs/only), but it has no trigger mechanism, so a rule there is read by nobody unless something routes to it. The alternatives — expandingAGENTS.mdagainst its budget, or pushing doctrine down into mechanism headers whereshell-retirement.rego's already lives — trade discoverability against currency. Pick one and say why; a row that surveys and does not decide is the punt this repository names. - Sequencing against the fact-family bundle (§2). This row moves
.claude/rules/policy-modules.md. The eight-row substrate bundle (CLOUD-1167, 856, 1200, 1203, 1187, 1188, 1154, 1171, 1172) ends with a single commit updating that file'sinput.tree.*key lists for every new family, and CLOUD-1206 edits therules-driftgate over the same file. Three changes, one small file, one of them a relocation. This row goes AFTER the bundle lands, or the bundle is told the path is moving — not both discovered at a rebase. Whichever way it is settled, the key-list content is the bundle's and the path is this row's; neither may quietly take the other's half. - Deliberately not in scope (§2). Relaxing rule 7's
docs/prohibition. Retiringrules-drift. Migrating.serena/memories/**, which is a second vendor surface with its own loading contract and its own row's worth of work — name it as inherited debt rather than folding it in. Writing gates for every (c)-class rule; this row classifies and files. - **Effect (§3). **
read. Prose and routing only; no gate's decision changes. - Output and exit (§5). No exit contract moves. If a pointer replaces prose, the mechanism it points at is unchanged — a rule's meaning must not shift in a move that is supposed to relocate it.
- **Commit / bump (§6). **
docs— no bump. - Test obligation (§7). The move itself is provable only by what survives it: **every rule this row relocates must still be reachable from **
AGENTS.mdin one hop, and no rule may be dropped in transit. That is CLOUD-994's recorded failure — an evicted sentence nothing detects — so the classification table is the fixture and the acceptance is that it is complete. Any (c)-class rule that gets a gate carries the ordinary shown able to fail obligation (CLOUD-418) on its own row, not here. - Blockers (§8). None.
relatedToCLOUD-1150 (the measured instance, whose gate is downstream of this decision), CLOUD-1132 (which moved content the wrong way), CLOUD-358 and CLOUD-994 (the same class, unowned), CLOUD-209 (the harness capability matrix behind the neutrality claim), CLOUD-1119.
Acceptance
- Every rule in
.claude/rules/**is classified (a)/(b)/(c), with the mechanism named for each (a) and a filed row for each (c). - Every core-crate test asserting a rules file's content is re-pathed, relocated or deleted with a stated reason, and
mise run test:cargois green after the move — the coupling is named on this row so it cannot be discovered mid-implementation. - The neutral home is decided, with the trigger question answered rather than deferred — a rule nothing routes to is not relocated, it is deleted.
- A session running under
Cursor,CopilotCli,GeminiCli,CodexClior the bareExitCodecontract can reach the two-shapes rule, the admission set and the lifecycle contract. AdvisoryReach.delivered_onis non-empty for more than one host, and the count is stated. A relocation that leaves it at 1/6 has moved prose rather than fixed reach, and no host'sdeclaredvalue moves without a primary-doc citation..claude/rules/**retains only class (b), or the row records exactly what it could not move and why.- No rule is lost in the move.
Filed after the owner asked why a project whose entire point is agent-neutrality keeps its own binding rules in one vendor's folder. The first answer given was that no neutral surface with a loading mechanism exists. That was wrong — corrected 2026-08-30 when the owner asked why Batten's own hook surface was not the answer. It is: the channel is built and measured, and it reaches one host because the advisory half of CLOUD-44 never shipped.
CLOUD-1176 The retirement campaign has one disposition — "port it into the core" — so it launders bash-era scope creep into `crates/batten`, and the ratchet's landed WITHDRAWAL arm has never been used
Why
Every row in this campaign assumes the successor is a thing built in
crates/batten. The rows say so in terms — CLOUD-1168's §2 requires the port
"conserve that logic unchanged, defect included***", and CLOUD-1145's §1 says
"the successor is a verb in crates/batten/"*. No row asks whether the
predicate belongs in Batten at all.
That is a machine for moving bash-era scope creep into the core, and it
contradicts the governing documents in four places.
What the spec says, and what the corpus actually contains
§2's command surface is a DECLARED INTENT, not a closed list — and this row said the opposite. §2's own CLOUD-244 note settles it: "SURFACE is authoritative for what ships and this section for what is intended", and spec::tests::the_emitted_surface_is_exactly_the_committed_row_set "fails when a verb is added, renamed, or re-parented — the prompt to reconcile this section in the same change." That is a drift gate that demands reconciliation, never a prohibition on new verbs. §2 also states plainly that the doc-vs-binary comparison "is not yet a gate: this section is out of tree" (CLOUD-95).
Measured against the emitted spec, 2026-08-30 — 18 rows disagree, so "closed" is false empirically as well as textually:
- §2 names, never shipped (6):
gate,guard,main,pr,questions,scan - shipped, §2 never names (12):
attribution,claim,commit,override,payload,perf,ready,receipt,semver,state,target,wiring - agreeing: 17
So the refusal this row is built on is §9 and rule 1, not §2. A new verb is not forbidden; a verb carrying consumer-specific behaviour is. Every conclusion this row draws survives on that footing — the board gates are CLOUD-*-shaped, hooks-wiring-check is hook-runner concern, ci-local-parity is a file-shape linter — and none of them needed §2 to be closed.
And §2 ALREADY NAMES the verbs this campaign has been re-inventing. pr create|ready|land|watch|dispatch (write), gate ("the one merge/ready bar"), main status|sync, scan. pr watch is the CI-wait shape, declared and unshipped; pr land is the landing loop; gate is the ready bar. A retirement whose successor is one of those is landing an intended verb, not widening the surface — which is the opposite of what this row told six others.
§9 and the document's closing line. "Consumer-specific behaviour is
reconstructed through extension surfaces, never baked into the core" — and
"Consumer-specific detail is intentionally absent — it belongs in the consumer's*
*own repo, never in the core."
Non-negotiable rule 1. No consumer-specific identifiers in crates/batten.
The board gates are CLOUD-*/Linear-shaped by construction, so porting them into
the core violates it outright rather than marginally.
The scope reminder. Batten is "not a hook runner, file-shape linter,*
**secret scanner, AST linter, or reference monitor." *hooks-wiring-check is
hook-runner concern; ci-local-parity is a YAML file-shape linter.
And §11 undercuts the single largest row. Completions and man pages are
derivations of the runtime-emitted spec "so the shipped binary and the generated*
**docs can never drift". derived-check is 210.8s — 16.9% of the bats corpus —*
**guarding a drift the design says cannot happen.
The mechanism already exists and the campaign does not use it
2026-08-30 — the missing half is now filed as CLOUD-1182, and it is narrower than this row. This row's predicate is "name your home from the five". CLOUD-1182's is one level down and is the reason the default has been "port it":
shell-retirement'shas_policy_surface(shell-retirement.rego:549-559) accepts a successor atpolicy/*.regoORcrates/batten/src/*.rsand does not distinguish them. So a CLI-verb port clears the retirement gate exactly as well as a module port, and the[rule.conserves]arm markers (// carried:,// subsumed:) record where the predicate went but never what kind of successor it became. Nine ports took the verb route and the ledger cannot tell you that.The two rows are complementary, not duplicates: this one chooses the home before design; CLOUD-1182 makes the choice legible in the ledger afterwards, so the count is reviewable instead of emergent. CLOUD-1182 is unblocked and is the cheaper of the two.
Measured while filing it: a tree-wide search for
CLOUD-1176returns zero hits — not in.claude/,batten.toml,policy/*.rego, orcrates/. That is expected for a Todo row, and it is also the thing to fix on landing: this row's doctrine has to end up somewhere a gate or a rules file can reach.**Related, and it is why the campaign's own numbers do not converge: **
bash-surface-not-growingforbids growth but has no descending ceiling, andbatten.toml:2701records the census going 141 → 148 acrossv0.0.106→v0.0.113while this campaign ran. CLOUD-1195 gives it a ceiling.**One home is now cheaper than this row assumed. **
batten check --rule <id>(surface.rs:797) already makes a[[rule]]row invocable by name — its doc comment says it exists precisely so "a gate ported out ofmise-tasks/… stays byte-identical and out of the changed-file set". Nineteenpolicy/*.regomodules ride it and none appears insurface.rs. So CONSUMER MODULE costs the surface nothing, which strengthens this row's case against porting.
conserves grew a WITHDRAWAL arm in CLOUD-1080 (Done), filed precisely
because "all three [arms] name a successor, so a deletion whose subject is gone*
*can only pass by lying or by a waiver config-lint refuses."
So DELETE is spellable today. What is missing is the doctrine that makes
anyone reach for it. Every row reaches for a port because the ratchet's shape
suggests one, which is the same defect one level up from CLOUD-908's
"retires_with conserves files, not logic".
The five homes
A disposition is chosen before a successor is designed:
| home | criterion | where |
|---|---|---|
| PRESET | generic policy any consumer wants | crates/batten/src/policy/presets/** (CLOUD-836) — five bundles ship today |
| CONSUMER MODULE | a real predicate carrying rule-1 identifiers | policy/*.rego in this repo |
| A §2 VERB — SHIPPED OR INTENDED | §2 already names it, whether or not it ships yet | shipped: doctor hooks, check, config lint, payload field, perf. **Intended and unshipped: pr watch, pr land, gate, scan, **main sync — landing one is fulfilling §2, not widening it |
| PRIOR ART | a command already on the operator's PATH does it | §9; CLOUD-455's shape |
| DELETE | the predicate should not exist | CLOUD-1080's withdrawal arm |
"Port into the core" is not on the list. Core work is confined to the fact*
*families the four live homes read — which is what CLOUD-1154/1167/1168/1170 are,
correctly understood: enabling substrate, not new surface.
Refinement — Ready (choose the disposition before designing the successor)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- **Authority boundary (§1).
.claude/rules/toolchain.md's retirement section
**and whichever gate the predicate below lands in. No **mise-tasks/program and
****no **tests/**/*.batsis edited or added —V-SHELL-RULE-EDITEDand
V-SHELL-RULE-ADDEDrefuse both. - Computable predicate (§2). A retirement PR names its subject's home from
the five above. A PR whose successor is acrates/battenverb not in the emitted spec is a finding unless it names the §2 row it fulfils or the §2 change it asks for — checkable againstbatten spec --format json's row set. This is a reconciliation demand, not a refusal (see the correction above): §2 declares six verbs that do not ship, so "absent from the spec" and "forbidden" are different answers and the gate must not conflate them. - The negative half is the point (§2). Without the refusal, "port it" stays
the default because the ratchet's admitted disposition implies one. A gate that
only records a home is a log, not a gate (rule 2). - Deliberately not in scope (§2). Choosing any individual program's home —
each is its own row. ChangingSubjectFacts::died, the admission clauses, or
CLOUD-1080's arm. Widening §2 — a row that needs a new verb argues for it there. Reconciling §2's 18-row disagreement with the emitted surface, which the correction above measures and which needs its own row: §2 itself says that comparison is not yet a gate because the section is out of tree. - **Effect (§3). **
read. - Output and exit (§5). Pointer-only: the subject path and the home it
declared, never a line of its body. Exit follows the0/1/2/3table. - **Commit / bump (§6). **
feat(policy)— patch until0.1.0if it lands a
gate;docs— no bump if the decision is doctrine-only. The row states which. - Test obligation (§7). Over the compiled binary in
crates/batten/tests/; no
.batsadded or edited. Shown able to fail per CLOUD-418: a retirement
declaring a home passes; one declaring a new verb absent from the emitted spec
is reported; and the anti-vacuity mirror — a retirement whose successor IS an
existing §2 verb is not reported, without which the first is satisfied by a gate
that flags every retirement. - Blockers (§8). None.
relatedToCLOUD-1080 (the withdrawal arm this makes
reachable), CLOUD-836 (presets), CLOUD-908 (conserves files not logic),
CLOUD-455 (the prior-art question nobody asks), CLOUD-1145 and CLOUD-1168 (the
two rows most changed by it), CLOUD-1151 (the wave owner), CLOUD-1174 (whose
generated table gains the home column), CLOUD-418.
Acceptance
- The five homes are written where a retiring author reads them, with DELETE
named as CLOUD-1080's arm rather than left implicit. - A PR proposing a new
crates/battenverb without a §2 row is reported. - At least one row is re-dispositioned as a consequence — the proof this is
doctrine rather than a preamble. CLOUD-1145 is the candidate: §11 says its drift
cannot happen. batten spec --format jsonstill emits exactly the committed row set.
Found by pressure-testing the campaign's own framing against the house style: every
filed row assumed a port, and four separate clauses say most of these predicates do
not belong in the core.
CLOUD-1239 `has_policy_surface` cannot name a PRESET, so the one generic-by-construction home is unspellable — 0 of 609 landed arms name one, and the gate's incentive runs into the core
Why
has_policy_surface (policy/shell-retirement.rego:549-559) admits two successor shapes:
has_policy_surface(path) if { ... startswith(name, "policy/"); endswith(name, ".rego") }
has_policy_surface(path) if { ... startswith(name, "crates/batten/src/"); endswith(name, ".rs") }A preset lives at crates/batten/src/policy/presets/** and is a .rego. It fails the first arm on the prefix and the second on the suffix, so it satisfies neither. The one successor shape .claude/rules/policy-modules.md calls generic-by-construction — and CLOUD-1176's five-homes table lists first — cannot be spelled in a retirement ledger at all.
Measured over the landed ledger at 5d38e0c2: of 609 arms, 0 name a preset, 110 name engine source, 18 of those retiring a whole file.
That distribution is not a taste for the core; it is a gate that had one. An author choosing the DRY home was refused and an author choosing crates/batten/src/*.rs was not, so the 110 are a habit the ledger shaped rather than 110 considered choices. This is CLOUD-1176's scope creep arriving through the mechanism meant to bound it, and it is why that row's "port it into the core is not on the list" has been losing to the ratchet's own shape.
The prose half, which is how this stayed invisible
Three sites describe the obligation as "a policy surface and a compiled-binary test", full stop — .claude/rules/toolchain.md (from c049a68c), the module's own withdrawn-arm comment, and CLOUD-1219's body. All three read as "a Rego module".
Under that reading CLOUD-1199's whole disposition — eleven gates retiring onto an existing verb, no module — has no spellable arm, and the author's only passing move is the false subsumed CLOUD-1080 exists to remove. Measured: a grooming session took two wrong turns in one turn on that sentence, including a false "the dispatched bundle is broken" conclusion, before reading the predicate instead of the prose.
Distinct from CLOUD-1182, which is the adjacent row
CLOUD-1182 makes a retirement declare its successor KIND so a verb port is reviewable. This row makes the preset shape expressible at all. 1182 assumes the arm set is right and adds a declaration on top of it; without this row its Rust arm still cannot distinguish a preset from engine source, because a preset never reaches that arm. They compose and neither subsumes the other.
Refinement — Ready (make the DRY home spellable, and say which home is which)
Refinement gate: Definition of Ready & Done. This body carries only specializations.
- **Authority boundary (§1). **
policy/shell-retirement.rego,.claude/rules/toolchain.md's retirement section, andcrates/batten/tests/retirement_doctrine.rsfor the mechanism. Nomise-tasks/program and notests/**/*.batsis added or edited —V-SHELL-RULE-EDITEDandV-SHELL-RULE-ADDEDrefuse both, and this row must not violate the ratchet it is repairing. - Computable predicate (§2). A retirement arm whose only policy surface is a preset (
crates/batten/src/policy/presets/**,.rego) is admitted; one naming no policy surface at all is still refused. That pair is the whole change — the arm is widened, never loosened. - **The prose half is a second predicate (§2). **
.claude/rules/toolchain.mdnames all three homes in preference order — consumer module → preset → engine source — with the test on the third being non-negotiable rule 1: a predicate naming a consumer fact may not land in the core. The gate cannot decide which home a retirement should have taken, so the ordering is written where the author reads it and the mechanism only holds the prose to the predicate. - Deliberately not in scope (§2). Declaring successor KIND (CLOUD-1182). Re-dispositioning any of the 110 engine-source arms — this row makes the alternative expressible, it does not re-litigate landed ports. Changing
has_binary_test, the admission clauses, orSubjectFacts::died. - Effect (§3).
read— a load-time predicate, nothing written. - Output and exit (§5). Unchanged: pointer-only, the retiring path and the missing obligation, never a line of a body. The
0/1/2/3table is untouched and no new verdict token is raised — this widens an existing admission rather than declaring a class. - Commit / bump (§6).
fix(policy)— patch until0.1.0. - Test obligation (§7). Both tiers. The module's own
test_rules pin the predicate;crates/batten/tests/retirement_doctrine.rsover the compiled binary holds the prose to it in both directions. Shown able to fail per CLOUD-418, three observed: (a) an arm naming only a preset is admitted — the case the arm exists for; (b) an arm naming no policy surface is still refused — the anti-vacuity mirror, without which (a) is satisfied by an arm admitting everything; (c) removing a home from the prose, or an arm from the module, turns the doctrine test red.
Acceptance
- A retirement whose only policy surface is a preset loads clean.
- A retirement naming no policy surface is still refused.
.claude/rules/toolchain.mdnames three homes with the rule-1 test on engine source, and a test fails if any is dropped.- The 0-of-609 measurement is restated from the ledger at landing time rather than quoted from this body.
Found while pressure-testing a dispatch prompt: the prompt said "no module needed", a reader objected that the ledger demands a policy surface, and reading has_policy_surface showed both were half right — engine source qualifies, and the preset home does not exist.
|
Warning Review limit reachedNext included review available in 37 minutes. View limit detailsLimit details: You’ve used the included review currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (4)
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/settings/billing. Comment |
0e5f793 to
8aff541
Compare
8aff541 to
6e0e7f6
Compare
6e0e7f6 to
4bc67f3
Compare
c836d01 to
7774f22
Compare
`mem:workflow/board-states` carried the facts this follows from — "In Review means already merged", "Done means released" — and never drew the consequence for grooming: a Todo row has shipped nothing, so its body is the spec and you correct it in place; an In Review or Done row's body describes code already on main, so a wrong design there gets a new row and a superseded reference, never a rewrite. The cut is landed-ness, not readership. Rewriting a shipped row's body makes the record disagree with the code, invisibly — the CLOUD-994 class. The opposite error had no name until now, and it cost this session two wrong turns on an Urgent, unclaimed, Todo row whose central premise was false: twice left uncorrected in favour of a spin-off row plus a supersede note, inventing a second authority over a design nothing had built yet and leaving the ready queue holding the wrong spec. No key trailer: this branch does not implement that row, it only cites it, and a Refs trailer would drag it to In Progress on the PR event. The branch is keyed by its first commit.
…s which `has_policy_surface` admitted a consumer module or engine source and nothing else, so a preset -- which lives at `crates/batten/src/policy/presets/**` and is a `.rego` -- failed the first arm on its prefix and the second on its suffix. The one successor shape `.claude/rules/policy-modules.md` calls generic-by-construction could not be spelled at all. The gate's incentive therefore ran the wrong way. Measured over the landed ledger: of 609 arms, 0 name a preset while 110 name engine source, 18 of those retiring a whole file. An author who picked the DRY home was refused and one who picked the core was not -- CLOUD-1176's scope creep arriving through the mechanism meant to bound it, and the reason those 110 are a habit rather than 110 considered choices. So this adds the preset arm, and names the choice where the author meets it. `.claude/rules/toolchain.md` carried "each names a policy surface and a compiled-binary test" with no discriminator, which reads as "a Rego module" and makes every retirement onto an existing verb look unspellable -- a misreading that cost this session two wrong turns in one turn, including a false alarm that a dispatched bundle was broken. It now names three homes in preference order and gives the test for the third: engine source is mechanism only, and non-negotiable rule 1 refuses a predicate there that names a consumer fact. `retirement_doctrine.rs` pins both directions, as it already does for the classifiers: the prose names three homes and the module declares three arms. The clause checks are whitespace-normalised, because these are prose and `mise run fmt` reflows them -- it did, on this very change. Refs: CLOUD-1176
…t exists The arm landed with textual coverage only: `retirement_doctrine.rs` counts three `has_policy_surface` definitions and greps the module for the presets prefix. Both pass over an arm carrying the WRONG prefix, so what shipped was evidence that a third arm exists and none that it decides anything. `test_a_mapping_naming_only_a_preset_is_admitted` is the behavioural half: a retirement whose only policy surface is `crates/batten/src/policy/presets/**` is admitted. `test_mapping_without_a_policy_surface_is_refused` directly above is its anti-vacuity mirror, so an arm admitting everything fails one or the other. Shown able to fail rather than asserted: inverting the expectation names the case and moves the tier to 312 passed / 1 failed, which is also what proves the case runs at all -- `policy test` prints counts and never lists a passing rule, so a green tier is not evidence that a newly added rule was among them. Refs: CLOUD-1176
7774f22 to
8b5b883
Compare
|
❌ The last analysis has failed. |
|
Superseded by #789, and closing rather than racing it — Why this branch was not simply rebased:
#789 carries this work rebased on current CLOUD-1176 is untouched and stays Todo. Generated by Claude Code |
has_policy_surfacecould not name a preset, so the one generic-by-construction successor home was unspellable in a retirement ledger — and the prose describing the obligation said "a policy surface" while meaning three different things.The finding
policy/shell-retirement.rego:549-559admitted two successor shapes:A preset lives at
crates/batten/src/policy/presets/**and is a.rego: wrong prefix for the first arm, wrong suffix for the second. It satisfied neither.Measured over the landed ledger at
5d38e0c2: of 609 arms, 0 name a preset, 110 name engine source, 18 of those retiring a whole file. That is not a taste for the core — it is a gate that had one. Choosing the DRY home was refused; choosingcrates/batten/src/*.rswas not.Why the prose needed saying out loud
Three sites describe the obligation as "a policy surface and a compiled-binary test", full stop, which reads as "a Rego module":
.claude/rules/toolchain.md(fromc049a68c), this module's own withdrawn-arm comment, and CLOUD-1219's body.Under that reading CLOUD-1199's whole disposition — eleven gates retiring onto an existing verb, no module — has no spellable arm, and the only passing move is the false
subsumedCLOUD-1080 exists to remove. It cost this session two wrong turns in one turn, including a false "the dispatched bundle is broken" alarm, before anyone read the predicate instead of the sentence.The corrected prose names three homes in preference order — consumer module → preset → engine source — with the test on the third being non-negotiable rule 1: a predicate naming a consumer fact may not land in the core.
What changed
policy/shell-retirement.regotest_a_mapping_naming_only_a_preset_is_admitted.claude/rules/toolchain.mdcrates/batten/tests/retirement_doctrine.rs.serena/memories/workflow/board-states.mdThe memory commit rides along as it always did. It carries no key trailer deliberately — this branch cites CLOUD-1152 without implementing it, and a
Refs:would drag that Urgent Todo row into In Progress on the PR event.Verification
retirement_doctrine.rs:177.:187,left: 2, right: 3.policy test: 313 pass, and the new case shown able to fail — inverting it gives 312 passed, 1 failed namingtest_a_mapping_naming_only_a_preset_is_admitted.Both mutations were run with
.git/batten-receiptscleared, for a reason worth recording: twomise run test:cargoinvocations returned exit 0 having run no tests, reportingreceipt 6a9ac634760b already covers these exact inputs— the key unchanged across an edit to.claude/rules/toolchain.md, and unchanged again with that file staged.step-receipt.sh:150givestest:cargospecs="$rust_specs", and:130iscrates Cargo.toml Cargo.lock batten.toml— no.claude/path, noAGENTS.md. Four test files read the committed prose at runtime and assert over it (retirement_doctrine,scanner_taxonomy,rules_builtin_claims,task_prose), so the task that runs them cannot observe the files they police. CI is unaffected (the cache neither hits nor records there), making it a local-only false green. The same class was already fixed once in that same file —batten.tomlis inrust_specsbecausetests/cli.rs'sthe_committed_*cases read it at runtime, "Measured, not hypothesised" — and the prose files were never added. Recorded on CLOUD-725.Separately,
policy testprints counts and never names a passing rule, so a green tier is not evidence a newly added rule was among them — which is why the new case was shown able to fail rather than assumed present. Unowned; adjacent to CLOUD-313.Two board corrections this PR's own history forced
Closes CLOUD-1150is gone, and it was wrong when written. That row's §1 owns the gate — retiringmise-tasks/rules-drift.shand its suite — and says so outright: "This row owns the gate, not the sentence." This PR retires nothing. CLOUD-1150 is already In Review via #774.A
Refs: CLOUD-1176in an earlier revision of this body completed an Urgent Todo row. The tracker attached this PR and set CLOUD-1176 to Done at 04:36:53 with nothing merged. It has been restored to Todo and the trigger removed. AGENTS.md is explicit that Done means released and is never the merge's to set, let alone a body edit's.The commit that body described —
e7ed838a, the sibling-edit admission count — is no longer in this PR. Main landed the same correction independently and now names four predicates where that commit named three, so it was dropped during rebase.Closes CLOUD-1239