Uh oh!
There was an error while loading. Please reload this page.
feat(hooks): guard the origin/main ENUMERATION half, and name the ls-tree idiom - #13480
Merged
Merged
Conversation
…tree idiom The "verify main with origin/main" rule covers reading file CONTENTS. It does not cover enumerating which files EXIST. A sweep that obeys the first half and not the second produces a zero that looks like a full-tree scan: every file it opens is read correctly, and files present on origin/main but absent at the working tree's HEAD are never iterated, so the loop cannot report them and cannot know it missed them. Three parts, mechanical first: - `.claude/hooks/guard-tree-enum.sh` — a PreToolUse guard that blocks one Bash command carrying BOTH a working-tree enumeration (`for NAME in <glob>`, `ls <glob>`, `find <path>`) AND an `origin/...` content read (`git show`, `git grep`, `cat-file`). Either half alone is ordinary and is allowed. A command that enumerates with `git ls-tree ... origin/...` is never blocked, however it then reads — the population/read cross-check must not be harder to write than the bug. Conservative: anything it cannot parse fails OPEN, and `OS_ALLOW_TREE_ENUM=1` is the deliberate exception, following the OS_ALLOW_MAIN_EDITS / OS_ALLOW_STASH family. - `.claude/hooks/guard-tree-enum.selftest.sh` — 36 cases, no network, no build. - `AGENTS.md` §9 — the canonical idiom as one more practice in the existing list, woven in line-neutrally (1162/1162, ceiling unchanged). "No hook backs this one" is narrowed to "the moving-ref half", which is what remains unbacked. The guard's header and its block message carry the transferable finding: a zero-hit control drawn from the same faulty file list validates the MATCHER, not the ENUMERATION. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
This was referenced Aug 30, 2026
REWORK. The previous commit funded ~230 bytes of new practice text by re-wrapping AGENTS.md §9's 9-line block from ~85-byte to ~115-byte width. That is 筹行 — re-wrap used to buy lines for content — banned by the maintainer ruling of 2026-08-17 as sharpened 2026-08-29: 「筹行(为内容购买行数)⛔ vs 独立密度修复(无内容购买)允许」 The ban binds this file, not only the pm-dispatch corpus. Decisively, the maintainer ruled TODAY (13:33Z, on #13052) a ceiling raise 1162→1164 to fund +2 lines in this very §9, whose lines wrap at ~90 bytes; if re-wrap funding were legal here that ruling would have been pointless. Landing the previous shape would have overturned that precedent from below. This commit restores the block to main's byte-exact wrap (blob 8802388) and keeps exactly one change, edited IN PLACE on its own line with no neighbouring line re-flowed: ⛔ **No hook backs this one** → ⛔ **No hook backs the moving-ref half** That repair is not optional and is not content purchase: a hook now DOES back the enumeration half, so the unqualified sentence became false in this PR. The line goes 87 → 98 bytes against the 120-byte cap, and `git diff` against main is 1 insertion, 1 deletion, zero net lines — AGENTS.md stays 1162/1162. The enumerate-from-the-ref idiom is NOT lost. It ships in this PR in the guard's header and in the guard's BLOCK MESSAGE — where the grading wanted the explanatory weight, and where an agent meets it at the moment of the mistake — and in objectui's AGENTS.md, which carries no ratchet. Its objectstack instruction-text home is deferred to the follow-up card being filed under option B of this card's open question 1, where the line can be funded legally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
os-zhuang
approved these changes
Aug 30, 2026
os-zhuang
marked this pull request as ready for review
August 30, 2026 15:50
os-zhuang
enabled auto-merge
August 30, 2026 15:50
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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.
Fixes#13305
Draft on purpose: governed surface (
.claude/**,AGENTS.md), human merge. Cross-repo card — the objectui half is objectstack-ai/objectui#6908, which references this card without a closing keyword.What was wrong
The
origin/mainreading rule covers file contents —git show origin/main:PATH,git grep ... origin/main. It does not cover enumerating which files exist. A sweep that obeys the first half and not the second produces a zero that looks like a full-tree scan:Every file it opens is read correctly. Files present on
origin/mainbut absent at the working tree's current HEAD are never iterated, so the loop cannot report them and cannot know it missed them.Measured on objectui, 2026-08-29: working tree 30 workflow files,
origin/main31. The one missing was.github/workflows/governed-surface-guard.yml, which declaresready_for_review— the answer to the exact question being asked. 10 PRs were flipped to ready on that reading, each goingcleantounstable, during a runner-capacity outage. That is why the card is p1: realised damage, not theoretical risk.A zero-hit control was run, and it passed.
pull_requestmatched 5 workflow files, so the method demonstrably worked — but those 5 came from the same faulty list, so it validated the matcher, not the enumeration.What this PR does, in the grading's order
3 first — the mechanical guard.
.claude/hooks/guard-tree-enum.sh, a new sibling hook so the existing guards' self-tests stay untouched. It blocks ONE Bash command carrying both a working-tree enumeration (for NAME in GLOB,ls GLOB,find PATH) and anorigin/...content read (git show,git grep,git cat-file). Either half alone is ordinary and is allowed — the pair is the signature. A command that enumerates withgit ls-tree ... origin/...is never blocked, however it then reads: the population/read cross-check must not be harder to write than the bug. Conservative parsing, fails open on anything it cannot confidently read, with the boundaries stated in the header rather than left to be rediscovered. Deliberate exceptionOS_ALLOW_TREE_ENUM=1, following theOS_ALLOW_MAIN_EDITS/OS_ALLOW_STASHfamily. Registered in.claude/settings.jsonnext to the other two Bash-matcher guards; the 36-case self-test is picked up automatically bylint.yml's discovery collector, so no workflow edit was needed here.2 — the canonical idiom,
git ls-tree --name-only origin/main DIR. In this PR it ships in the guard's header and in the guard's block message, and in objectui'sAGENTS.md(that repo carries no line ratchet, so the sentence is funded there and that half stands). It is deliberately not added to objectstack'sAGENTS.mdhere: this file is at 1162/1162 with zero headroom, and the only ways to fund a sentence are a ceiling raise (a maintainer ruling) or re-wrap (banned, see above). Its objectstack instruction-text home is deferred to the follow-up card filed under option B of this card's open question 1, which will carry it into.claude/agents/os-dev.md,pm-dispatch references/platform-readings.mdand §9 together, once those faces free and the funding is ruled.1 last, and subordinate. The explanatory weight sits in the guard's header and, more importantly, in its block message — where an agent meets it at the moment of the mistake rather than in prose that gets skimmed. Both carry the grading's transferable sentence verbatim:
Line budget — 1162/1162, and nothing was bought
AGENTS.md1162 in, 1162 out;CLAUDE.mduntouched at 86. No re-wrap, no reflow, no ceiling raise. The entireAGENTS.mddiff against main is one line changed, 1 insertion / 1 deletion, zero net lines — a single correctness repair edited in place, with no neighbouring line touched:That repair is not content purchase and is not optional: a hook now does back the enumeration half, so the unqualified sentence became false within this PR. The line goes 87 → 98 bytes against the gate's 120-byte cap. Every other line of
AGENTS.mdis byte-identical to main's blob8802388ed5— verified line by line, not by eyeball: 1163 lines on both sides, exactly 1 differing.Because nothing is funded here, the pending A/B/C on #13052 (contemplating 1162 → 1164 on this same §9) is not a dependency in either direction.
CLAUDE.mddeliberately untouched: it inlines only the four rules that must never be missed, and this is not one of them.Verification, at
b201e2a40Reverse verification, leg (a) — the guard fires on the measured signature. Neutering
is_glob_wordpredicted exactly 8 of 9 block cases flipping toallow, with thefindcase staying blocked because that branch does not consult the helper. Observed:28 passed, 8 failed,findabsent from the failures, and the verbatim objectui loop among them. Mutation confirmed on disk by marker count in both directions and a changed blob hash; restored withgit checkout HEAD -- PATH, proven byte-identical by blob-hash equality plus emptygit diff HEADand emptygit status; self-test back to36 passed, 0 failed.Leg (b) — the line ratchet is really measuring. Re-run at this head, since the rework changed the tree the earlier leg measured. A +1-line ablation on
AGENTS.mdpredicted RED. Observed, quoting the gate's own verdict line:Restored: blob-hash equal to HEAD,
git diff HEADempty,git statusempty, and✓ check-skill-line-ratchet: AGENTS.md is 1162 lines (ceiling 1162; headroom 0).Gates. Re-derived at this head from the actual diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(same 12 families), all re-run on the final commit, every exit code captured by redirect-then-capture and never through a pipe — all exit 0:check:agent-test-spelling·check:bash32-floor·check:doc-authoring·check:doc-formula-expressions·check:docs-audit-scope·check:pm-governed-merges·check:pm-governed-prose·check:pm-skill-id-lint·check:pm-skill-ratchet·check:required-contexts·check:skill-frame-sync·check-required-contexts.mjs, pluscheck-nul-bytes.mjsand the new self-test.Quoted verdict lines:
check:doc-formula-expressionsfirst reportedPREREQUISITE NOT MET(an unbuilt workspace package), which is not measured rather than red — it is reported green only after building@objectstack/formulaand@objectstack/lintand re-running it to exit 0.No changeset — nothing is published from any package (
.claude/**andAGENTS.mdonly), so this takes theskip-changesetlabel.Also filed, not fixed here
objectstack-ai/objectui#6906 — objectui's
hook-selftests.ymlhand-enumerates the self-tests it calls itself the standing caller for, so a new matrix ships uncalled. Found by hitting it: the mirror PR had to add its step by hand, where this repo's discovery collector needed nothing. Same defect class as this card, different file; redesigning that runner is its own change.Dev session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
Generated by Claude Code