Skip to content

feat(hooks): guard the origin/main ENUMERATION half, and record the incident in AGENTS.md - #6908

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13305-enumeration-origin-main
Aug 31, 2026
Merged

feat(hooks): guard the origin/main ENUMERATION half, and record the incident in AGENTS.md#6908
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-13305-enumeration-origin-main

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Part of objectstack-ai/objectstack#13305 — the objectui half. The objectstack PR carries the closing keyword; one card, one closer. Draft on purpose: governed surface (.claude/**, AGENTS.md), human merge.

What was wrong

The origin/main reading rule covers file contents. It does not cover file enumeration. 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.

Measured in this repository on 2026-08-29:

  • working tree, ls .github/workflows/*.yml piped to wc -l30
  • git ls-tree --name-only origin/main .github/workflows/ filtered to .yml31
  • the missing one: .github/workflows/governed-surface-guard.yml, whose line 36 declares types: [opened, synchronize, reopened, ready_for_review]

The question being asked was "would flipping these drafts to ready trigger any CI?". The answer to it was the file left out of the enumeration. 10 PRs were flipped to ready on that reading, each going from mergeable_state: clean to unstable, during a runner-capacity outage.

The zero-hit control that was run did not catch it, and passed.pull_request matched 5 workflow files, so the method demonstrably worked — but those 5 came from the same faulty list, so the control validated the matcher, not the enumeration.

What this PR does — mechanical first, prose subordinate

1. .claude/hooks/guard-tree-enum.sh (+ self-test). A PreToolUse guard blocking ONE command that carries 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. A command enumerating 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. Registered in .claude/settings.json alongside the other two Bash-matcher guards.

Mirrored from objectstack with the discipline guard-shared-stash.sh already documents — verdict logic case-for-case identical so the two repos' guards cannot drift. Proven, not asserted: stripping comments and blanks leaves 193 executable lines on both sides, and the only differing line is message text inside the block heredoc.

2. AGENTS.md. A new subsection placed directly under the existing 阳性对照 rule, because that is the rule that failed. It carries the incident's numbers, the canonical idiom, and the transferable sentence verbatim:

一个对照必须有能力因为你担心的那个原因而失败;同源对照结构上做不到。

No line ratchet applies in this repo (checked, not assumed — this repo has no check-skill-line-ratchet equivalent); AGENTS.md goes 448 to 489 lines.

3. .github/workflows/hook-selftests.yml — a declared surface expansion. Not in the dispatched file surface, and done anyway because skipping it would ship a self-test that nothing runs. This workflow's steps are a hand-kept enumeration of .claude/hooks/*.selftest.sh, so the new matrix would have been silently uncalled. objectstack's twin discovers with find and needed no edit. The underlying shape is not redesigned here — filed as #6906 instead.

4. scripts/dependabot-merge-gate.mjs — one word. Its description of that workflow said "the two ... matrices", which step 3 makes stale. De-counted rather than re-counted, matching that file's own documented stance on hand-copied enumerations.

Verification, at 0a30e47

Reverse verification on the mirrored hook — neutering is_glob_word predicted exactly 8 of 9 block cases flipping to allow, with the find case staying blocked because it does not consult that helper. Observed: 28 passed, 8 failed, the find case absent from the failures. Mutation confirmed on disk by marker count and by a changed blob hash; restore proven byte-identical (same hash) and self-test back to 36 passed, 0 failed.

Gates, each exit code captured by redirect-then-capture, never through a pipe:

  • check:control-bytes, check:doc-fences, check:governed-queue-guard, check:skills-paths, check:shell-escape-residue, check-changeset-presence.mjs, check-doc-links.mjs — all exit 0
  • all three hook self-tests green, including the new guard-tree-enum selftest: 36 passed, 0 failed
  • scripts/__tests__ suites for the edited gate script and the touched workflows — Test Files 6 passed (6), Tests 143 passed (143) (dependabot-merge-gate, merge-queue-reporting, check-governed-queue-guard, ci-cd-pipeline-doc, lint-workflow, check-shell-escape-residue)

No changeset: check-changeset-presence.mjs exits 0 — no package source is touched. The skip-changeset label is deliberately not applied: in this repository it is a phantom that no workflow or gate reads, and scripts/__tests__/ci-cd-pipeline-doc.test.ts pins it that way.

Dev session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de


Generated by Claude Code

… incident here
This repository is where the defect was measured on 2026-08-29. The
"verify main with origin/main" rule covers reading file CONTENTS; it does not
cover enumerating which files EXIST. A working-tree glob feeding per-file
origin/main reads yields a zero that looks like a full-tree scan: 30 workflow
files in the working tree vs 31 on origin/main, and the missing one was
governed-surface-guard.yml, which declares ready_for_review — the answer to the
very question being asked. 10 PRs were flipped to ready on that reading during a
runner-capacity outage.
- `.claude/hooks/guard-tree-enum.sh` (+ self-test) — mirrored from objectstack,
verdict logic case-for-case identical so the two repos' guards cannot drift
(193 executable lines on both sides; only the incident's framing and the
self-test's example paths are localised). Blocks one command carrying BOTH a
working-tree enumeration AND an `origin/...` read; either half alone, and any
command enumerating with `git ls-tree ... origin/...`, is allowed. Fails OPEN
on anything it cannot parse. `OS_ALLOW_TREE_ENUM=1` is the deliberate exception.
- `AGENTS.md` — a new subsection extending the 阳性对照 rule directly above it,
since that is the rule that failed: the zero-hit control that was run passed,
because it was drawn from the same faulty file list, so it validated the
matcher rather than the enumeration.
- `.github/workflows/hook-selftests.yml` — the new self-test had to be added BY
HAND, because the steps here are a hand-kept enumeration of
`.claude/hooks/*.selftest.sh` rather than a discovery loop like objectstack's.
Without this the mirrored guard would ship with its matrix uncalled. The
underlying shape is filed separately rather than redesigned here.
- `scripts/dependabot-merge-gate.mjs` — one word: its description of this
workflow said "the two ... matrices", which the step above makes stale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de
@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

Queue dequeue root-caused — not this PR's failure; re-queueing once.

Failing check: merge-queue CI, Test (shard 4/4), run 33352591198 on gh-readonly-queue/main/pr-6908-b84dc185: exactly 1 of 6,393 tests failed — packages/plugin-gantt/src/ObjectGantt.referenceArms-6837.test.tsx:257 (getByTestId('gantt-view') while the DOM still shows "Loading Gantt chart…"). Every other queue check was green.

Why it is not this PR's:

  1. The test did not exist at this PR's base e28fbf92 — it landed on main 2026-08-30T18:38Z via fix(plugin-gantt,plugin-tree): drop the undeclared referenceTo arm from both target chains #6922, so this PR's fully green head run never executed it and could not have broken it.
  2. This diff is hooks + AGENTS.md + a workflow matrix + one word in a gate script: zero runtime code, zero vitest files (shard composition unchanged), nothing anywhere near plugin-gantt.
  3. The SAME test passed the merge queue's CI fifteen minutes earlier (run 33352366338, pr-6936 queue branch) on a tree whose only differences from the failing one are fix(types): one authority for ActionSchema and the Breadcrumb pair #6936's types change and this PR. A pass/fail split on materially identical content is nondeterminism in the test — and its shape (:256 await mount:257 synchronous getByTestId) is a textbook async race.

Actions: the race is filed as #6953 for the plugin-gantt owners (the fix belongs in that test, not in this PR — widening this PR into plugin-gantt would be scope creep on a governed-surface change); this PR is re-queued via auto-merge as the one legitimate re-run. If the queue fails on the same test a second time, that re-confirms #6953, raises its priority, and this PR stays parked on that fix — escalated to the maintainer then.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdconfigurationdocumentationImprovements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@zhuangjianguo@os-zhuang@claude