refactor(rules): five worktree bullets each re-stated the same silent-failure frame (#193) - #458
Merged
Merged
Conversation
…-failure frame (#193) `scripts/tests/test_rules_size.py` said eviction had been run to completion and that the answer to the next real rule was consolidation, not another ceiling bump — naming "the four scattered worktree bullets" as the candidate. PR #447 then made it five. This does the consolidation it asked for. The cluster stated three things more than once: - the `git worktree add … -b <branch> origin/<main-branch>` recipe, written out twice (sync-a-branch, and again for cross-repo). Now once, in the `git -C <repo>` form that covers both, referenced by the cross-repo rule. - "never two file-modifying agents in one checkout", stated as a hazard and again as a prohibition inside the same bullet. Now once as each. - the silent-failure framing ("no error, the worktree is created successfully"), spelled out separately by the cross-repo, session and .envrc bullets. Now hoisted once. Structurally: three separate "a worktree does not isolate X" rules became one frame — **a worktree isolates a working DIRECTORY only** — with three enumerated unisolated surfaces, the REPO it is built from, the SESSION, and the ENVIRONMENT. Each keeps its own 🔴, its own imperative, its own failure shape and its own `→ archive:` pointer, so none of them depends on the frame to carry its scope, and a fourth surface can join the list without re-stating it. The frame is also a correctness fix: the old text asserted "A worktree isolates the REPO, not the SESSION" while the bullet directly below it said the worktree is built from the wrong repo. Both cannot be true. "isolates a working DIRECTORY only" is the archive's own wording and is wider than either. claude/RULES.md 33,300 -> 33,229 B (-71). That is a small number and it is the finding, not a shortfall: every surviving clause is scope-bearing, so the only bytes available were genuine restatement. Recorded in the gate module so nobody reads this as evidence that another -71 B is sitting there. Nothing moved to the archive — there was no un-evicted narrative left in the cluster to move, which is the same "eviction has been run to completion" the module already recorded. The three archive sections had their `*Supports:*` quote lines updated to match the new core wording. Qualifiers considered for trimming and deliberately KEPT (each would have let the shortened rule pass while the hazard survived in a shape the original covered): - "Two failure modes, only one loud" + both modes. The point of the pair is that you would not necessarily notice; neither mode alone carries it. - "that still satisfies the isolation mandate, so this is not an exception to it" — without it, "do NOT pass the flag" reads as licence to skip isolation. - "file-modifying" and "in one checkout" on the never-two prohibition. A bare "never two" was written, then reverted: it silently dropped both qualifiers. - "Read-only agents (audits/research/exploration) don't need one" — the parenthetical is the routing surface that makes "read-only" operational. - "working in a tree where its brief makes no sense" — arguably subsumed by the two failure modes, which is exactly the reasoning the #447 trim used. - the `../<repo>-<topic>` path convention, dropped when the recipe was unified and restored once a cold read caught it. MAX_BYTES deliberately NOT ratcheted down. The headroom predates this change, and tightening the gate is a policy call that belongs in its own commit. Verification: all 5 functions in test_rules_size.py pass, and each was negative-controlled — ceiling forced below current size, headroom forced above the gap, RULES_MD and ARCHIVE_MD pointed at a missing file, and a bogus `→ archive:` anchor injected — each going red with its own message. A separate claim-coverage checker asserts all 56 distinct claims from the original block are still present and records the 7 deliberate rewordings with their reasons; it was itself controlled (empty file -> all 56 reported missing; five single-claim deletions -> red; unmutated -> green). Authoritative gate: TOTAL collected=9429 passed=9428 failed=0, RESULT: PASS (exit=0). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…roduced Audit of #458 found no lost claims -- the consolidation itself is sound -- but two 🟡s in the NEW frame text, both of the "a frame is a claim too" class this file keeps getting bitten by. Fixed here rather than in a follow-up, because the frame is what a skimming agent retains. 1. The verb fitted one of its three surfaces and INVERTED the sign on another. "three surfaces it does NOT isolate" is right for SESSION (scratchpad and branch namespace leak in). For REPO the failure is TARGETING, not isolation -- the worktree is perfectly isolated, just of the wrong repo. For ENVIRONMENT it read backwards: "does not isolate the environment" implies the environment is SHARED, when the hazard is that it is ABSENT (.envrc never comes with the checkout). That false implication is precisely the belief the .envrc rule exists to kill. Now "the surfaces it does NOT hand you", which covers all three. 2. "each failing SILENTLY" was hoisted from ONE bullet and asserted of three. Only the cross-repo bullet ever claimed it; grep of origin/main confirms neither the SESSION nor the .envrc bullet contains `no error`, `created successfully` or `silently`. Worse, the ENVIRONMENT sub-bullet documents two LOUD manifestations in its own text -- a toolchain binary "missing", and an un-`direnv allow`ed .envrc erroring on every `cd`. The claim that IS true of all three is that `worktree add` succeeds without warning you, so that is what it now says. 3. "three surfaces" read as a CLOSED enumeration. A fourth already exists eleven lines later -- the machine/process surface at the pgrep/pkill bullet, where a box-wide pattern reaches a sibling agent's processes (-> archive: sibling-agent-kill); the archive's own wording is "not a private repo and not a private machine". The frame now says the list is not closed, and the gate module records why. Also corrects three comment claims in scripts/tests/test_rules_size.py, since a comment is a claim too and this module is where the next maintainer will look: - "five bullets became one recipe + one frame" was 5 -> 2; it is 5 -> 3 (the base-clone re-sync bullet stayed top-level). - the ~250 B saving is an ESTIMATE of a hypothetical, not a measurement, and is now labelled as one. - the comment restated MIN_HEADROOM_BYTES as a literal ("~1,970 vs the 900 floor"), which the module's own docstring forbids for drift reasons. Replaced with a pointer to the constants. And requotes the three `*Supports:*` lines in RULES-ARCHIVE.md: two of them did not match the core verbatim (ASCII `--`/`->` for `—`/`→`, and a capitalisation), so an archive->core grep missed. All four quoted substrings now grep to exactly 1 hit. Nothing enforces this direction -- test_every_archive_pointer_resolves only checks core->anchor -- so it is eyes-only. RULES.md 33,229 -> 33,264 B (+35) against the ceiling and headroom floor the module owns; all 5 tests pass. MAX_BYTES and MIN_HEADROOM_BYTES untouched, no assert changed -- the audit proved the gate module's AST identical to main's before this commit, and this commit keeps it comment-only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delta audit of 7eada94..09e78ab found the fix round sound on every claim -- and one regression it introduced, inside the very comment block whose purpose was comment accuracy. The block still read "Result: 33,300 -> 33,229 B" and "That -71 B is the honest yield" after the frame reword had spent 35 B back. A reader at main measures 33,264 against a comment asserting 33,229, with the headline yield overstated roughly 2x. Now records both numbers and why they differ: the consolidation landed 33,229, the audit found the new frame verb described only one of its three surfaces correctly, and the reword cost 35 B. Both ship in one merge, so -36 B is what the tree sees. The audit also settled the question I raised against my own fix -- whether changing the frame verb to suit REPO and ENVIRONMENT had broken SESSION. It had not: "does NOT hand you" is better for REPO (the failure is targeting, not isolation) and fixes the sign inversion on ENVIRONMENT, and while the old verb was marginally more direct for SESSION, no claim is lost -- the sub-bullet still spells the leakage out verbatim. Silence is preserved and arguably strengthened: "a SUCCESSFUL `worktree add`" names the moment the silence occurs, which is what defuses the contradiction with ENVIRONMENT's two loud downstream tells. Verified after: RULES.md 33,264 B, all 5 tests pass, and the gate module's ast.dump SHA is unchanged from origin/main (a5c3e59bea8d9b87) -- still provably comment-only, no assert or constant touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Aug 15, 2026
…d for without narrowing one (#193) (#489) * docs(rules): three measured lessons folded into existing bullets, paid for without narrowing one (#193) The consolidation half of #193 already landed as #458 (five worktree bullets became three). What was left was the harder half: fold three lessons measured 2026-08-14 into that cluster without a ceiling bump. All three attach to bullets that already exist, because that is where a reader already is when the hazard fires: - `cp -a` of a worktree gives ZERO git isolation — a worktree's `.git` is a FILE holding `gitdir: …`, so the copy shares the ORIGINAL's git dir, index, refs and reflog, and a `git commit` inside it lands on the real branch. This is the fourth surface under "a worktree isolates a working DIRECTORY only", and it cost 394 B — its own text only, reusing the frame. It also sharpens the SESSION surface, whose own advice ("restore from `cp -a`") is what tells an agent to make these copies. - A guard on PROSE is walkable by rewording — extends "SPELLED rather than STRUCTURAL" (same question, new artifact type: when the thing under test IS a sentence, "assert the STATE" has no referent, so pin the whole normalised string). - A fixture whose value EQUALS the constant it tests — extends "pick fixtures whose fields are pairwise distinct" in the mutation bullet (same collision, different pair: fixture vs constant, not field vs field). The last two reuse the archive anchor already on their bullet, so neither added a `→ archive:` tag. Evidence for all three is in RULES-ARCHIVE.md. The additions overshot the budget by 202 B, reclaimed without narrowing anything: the stash section stated "for ANY reason" three times, so the third was dropped and the other two kept verbatim; and the pkill bullet's incident numbers moved to `sibling-agent-kill`, which already held them verbatim. The clause saying `/proc/<pid>/cwd` cannot separate two agents that both sit in the base clone was NOT touched — PR #447 trimmed exactly that and re-opened the incident. Numbers are owned by scripts/tests/test_rules_size.py; its comment ledger records this pass, and the 2026-08-13 entry's explicitly-unmeasured prediction (that a fourth hazard would attach for the cost of its own text) is now measured at one instance. The cold read caught the only regression: "never two" had been shortened from "never two file-modifying ones" and read as forbidding a second read-only agent that the previous sentence permits. No test performs that check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(rules-gate): the comment telling the next shaver what is load-bearing under-reported the one bullet this PR shortened Audit findings on this PR. Both are claims in the gate's OWN source — the file a future author reads to decide what is safe to trim — and both cost zero `RULES.md` bytes, which is the point: the gated file is untouched. 🔴 THE COMMENT SAID A BULLET WAS UNTOUCHED WHEN IT SHIPPED 18 B SHORTER It recorded the caught regression ("never two" -> "never two file-modifying ones", 20 B restored) but not that the shipped wording is the MIDDLE position, 18 B below what was there before. To the next person sizing that bullet up it read as untouched, which is exactly backwards for the one bullet whose over-shortening this PR had to catch by cold read. Now states the shipped wording, why the antecedent still survives (the full form appears earlier in the same bullet), and that shaving further breaks it. 🔴 "202 B RECLAIMED WITHOUT NARROWING" MISATTRIBUTED ITS OWN SOURCES An audit measured it: only **133 B** came out of existing rules (stash -89, pkill -26, worktree -18). The remaining ~69 B came from trimming the three NEW additions. The comment named only the two existing-rule sources, so it read as though the whole 202 B came from prose already in the file — making "reclaim without narrowing" look about 50% cheaper as a recipe than it is. ## The other direction of the anchor check was asserted in prose and gated nowhere This module claimed anchors "resolve both directions" while only computing `referenced - anchors`. The inverse — an archive section nothing points at — is the same rot one step over, and this module's own `_archive_anchors` docstring already argues the case: *"a hand-maintained list drifts, and a drifted list steers a maintainer into duplicating an entry that already has a home."* An orphan is how that happens: the evidence exists, nothing routes to it, so the next author writes it again in the core, where bytes are the scarce thing. ⚠ **Labelled as an INVARIANT GUARD, not counted as regression coverage.** It was green the moment it was written (0 orphans), so it has never caught the bug it describes. Watched to fail rather than assumed: - inject `## zzz-orphan-control` that nothing references -> RED with its own message, at its own assertion; - rename the same section `retired-zzz-orphan-control` -> GREEN again. The second is the control that matters: it proves the `retired-*` exclusion is live rather than decorative. A prefix exclusion nobody exercises is how a guard quietly stops covering the case it was scoped around. Verified: `RULES.md` and `RULES-ARCHIVE.md` are byte-identical to the parent commit — this spends none of the 32 B of slack the PR lands on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes clawgate Task #193.
scripts/tests/test_rules_size.pystated in its own comments that eviction had been run to completion, and that the answer to the next real rule was consolidation, not another ceiling bump — naming "the four scattered worktree bullets" as the obvious candidate. PR #447 then added two recovered rules, taking the cluster to five. This does the consolidation it asked for.What was actually duplicated
git worktree add … -b <branch> origin/<main-branch>recipegit -C <repo>form that covers both; cross-repo references it.envrcbulletsWhich bullets merged into which
Three separate "a worktree does not isolate X" rules became one frame with three enumerated surfaces:
Each surface keeps its own 🔴, its own imperative, its own failure shape and its own
→ archive:pointer — so none depends on the frame to carry its scope, and it survives being quoted in isolation. A fourth surface can join the list without re-stating the frame.Left structurally alone: the sync-a-branch bullet (now holding the canonical recipe), the take-another-ref bullet, the base-clone re-sync bullet, and the
pgrep/pkillbullet in Shell & Tooling — its/proc/<pid>/cwdclause is the one PR #447's audit had to restore, and it is untouched here.A correctness fix, not only a byte one
The old text asserted "A worktree isolates the REPO, not the SESSION" while the bullet directly beneath it said the worktree is built from the wrong repo. Both cannot be true. "isolates a working DIRECTORY only" is the archive's own wording and is wider than either.
Byte delta
claude/RULES.md33,300 → 33,229 B (−71). Headroom 1,900 → 1,971 (floor 900).That is a small number and it is the finding, not a shortfall: every clause that survived is scope-bearing, so the only bytes available were genuine restatement. The frame itself costs ~78 B; it is paid for by ~150 B of real dedup. Recorded in the gate module so nobody reads this as evidence that another −71 B is sitting there — it is not, and the next real rule still costs ceiling.
What the 71 B bought: a fourth worktree hazard now attaches to the enumerated list for the cost of its own text, instead of arriving as a sixth sibling bullet re-stating the frame (~250 B of restatement, measured against the four it would have copied).
MAX_BYTESdeliberately NOT ratcheted down. The slack predates this change; tightening the gate is a policy call that belongs in its own commit where it can be argued on its merits.Nothing moved to the archive
There was no un-evicted narrative left in the cluster to move — the same "eviction has been run to completion" the module already recorded. The three archive sections had their
*Supports:*quote lines updated to match the new core wording, andtest_rules_size.py's comment quoting the now-superseded rule name was corrected (a comment is a claim too).Qualifiers considered for trimming and deliberately KEPT
Each would have let the shortened rule pass while the hazard survived in a shape the original covered — the exact failure mode
test_rules_size.py:128-133forbids and that the #447 trim hit.isolation: "worktree"" reads as licence to skip isolation entirely.../<repo>-<topic>path convention. Dropped when the recipe was unified into the-Cform, restored once a cold read caught that it was concrete guidance and not framing.Verification
scripts/tests/test_rules_size.pypass (called directly —python3on PATH has no pytest).OVER its hard ceiling), headroom forced above the gap (no working headroom left),RULES_MD→ missing file,ARCHIVE_MD→ missing file, and a bogus→ archive: no-such-anchor-hereinjected.→ archive:anchor still resolves (test_every_archive_pointer_resolves).nix build .#checks.x86_64-linux.pytests, read by content:TOTAL collected=9429 passed=9428 skipped=1 failed=0, all 17 targets above floor, no=== FAILURES ===, no timeout panic,RESULT: PASS (exit=0).../<repo>-<topic>convention, the bare "never two", "the flag" without an antecedent inside the sub-bullet, and the lost bold on the.envrcconsequence clause.🤖 Generated with Claude Code