Skip to content

feat(hooks): guard the origin/main ENUMERATION half, and name the ls-tree idiom - #13480

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-13305-enumeration-origin-main
Aug 30, 2026
Merged

feat(hooks): guard the origin/main ENUMERATION half, and name the ls-tree idiom#13480
os-zhuang merged 2 commits into
mainfrom
claude/issue-13305-enumeration-origin-main

Conversation

@claude

@claude claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

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.

Reworked at b201e2a40. The first revision funded a new AGENTS.md practice sentence by re-wrapping §9's 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 block is now restored to main's byte-exact wrap and the practice sentence is dropped from this PR. See "Line budget" and "Route 2" below.

What was wrong

The origin/main reading rule covers file contentsgit 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:

for f in .github/workflows/*.yml; do            # file list from the WORKING TREE
  git show "origin/main:$f" | grep -q PATTERN   # contents from origin/main
done

Every file it opens is read correctly. Files present on origin/main but 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/main 31. The one missing was .github/workflows/governed-surface-guard.yml, which declares ready_for_review — the answer to the exact question being asked. 10 PRs were flipped to ready on that reading, each going clean to unstable, 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_request matched 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 an origin/... 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 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 parsing, fails open on anything it cannot confidently read, with the boundaries stated in the header rather than left to be rediscovered. Deliberate exception OS_ALLOW_TREE_ENUM=1, following the OS_ALLOW_MAIN_EDITS / OS_ALLOW_STASH family. Registered in .claude/settings.json next to the other two Bash-matcher guards; the 36-case self-test is picked up automatically by lint.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's AGENTS.md (that repo carries no line ratchet, so the sentence is funded there and that half stands). It is deliberately not added to objectstack's AGENTS.md here: 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.md and §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.md 1162 in, 1162 out; CLAUDE.md untouched at 86. No re-wrap, no reflow, no ceiling raise. The entire AGENTS.md diff 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:

-staging. ⛔ **No hook backs this one** — safe and unsafe spellings are both ordinary
+staging. ⛔ **No hook backs the moving-ref half** — safe and unsafe spellings are both ordinary

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.md is byte-identical to main's blob 8802388ed5 — 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.md deliberately untouched: it inlines only the four rules that must never be missed, and this is not one of them.

Verification, at b201e2a40

Reverse verification, leg (a) — the guard fires on the measured signature. Neutering is_glob_word predicted exactly 8 of 9 block cases flipping to allow, with the find case staying blocked because that branch does not consult the helper. Observed: 28 passed, 8 failed, find absent 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 with git checkout HEAD -- PATH, proven byte-identical by blob-hash equality plus empty git diff HEAD and empty git status; self-test back to 36 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.md predicted RED. Observed, quoting the gate's own verdict line:

✗ check-skill-line-ratchet: AGENTS.md is 1163 lines; the ratchet ceiling is 1162.

Restored: blob-hash equal to HEAD, git diff HEAD empty, git status empty, 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, plus check-nul-bytes.mjs and the new self-test.

Quoted verdict lines:

✓ check-skill-line-ratchet: AGENTS.md is 1162 lines (ceiling 1162; headroom 0).
✓ check-skill-line-ratchet: AGENTS.md: widest table row is 1081 bytes (pin 1081; headroom 0).
✓ check-bash32-floor: 24 tracked shell file(s) under scripts/**, .claude/hooks/**, .githooks/** name no bash 4+ construct outside a comment, a guarded ${VAR:-} read, or a non-command position.
✓ check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md) and claim no others.
guard-tree-enum selftest: 36 passed, 0 failed

check:doc-formula-expressions first reported PREREQUISITE NOT MET (an unbuilt workspace package), which is not measured rather than red — it is reported green only after building @objectstack/formula and @objectstack/lint and re-running it to exit 0.

No changeset — nothing is published from any package (.claude/** and AGENTS.md only), so this takes the skip-changeset label.

Also filed, not fixed here

objectstack-ai/objectui#6906 — objectui's hook-selftests.yml hand-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

…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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants