fix(pm): the charter latest-touch reading is taken depth-independently — a shallow clone's git log names a commit that never touched the path - #18327
Conversation
…y — git-history.mjs touch proves the sha or refuses On a shallow clone `git log -1 <ref> -- <path>` names the graft boundary as the last touch of a path the boundary never changed: the boundary's object still names its parent, the graft hides that parent from traversal, and git diffs the boundary against the empty tree, so every path in its tree reads as touched there — a real sha, exit 0, no warning. Measured at two depths on a constructed repo and on a 50-deep clone of this one; the `git show --stat` leg the card proposed is fooled by the same empty-tree diff. `git-history.mjs touch --path=<file>` answers the last-touch sha only when every parent the commit object names is present locally and the diff against them touches the path; otherwise it deepens with `--deepen=N` (additive, doubling), then `--unshallow`, then refuses with exit 2 and empty stdout. The open-round mutex line in SKILL.md names it, re-keyed in place at 117 B. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
Contract reviewServed-tier: Reviewing seat: ① Derived judgments
② Semver levelNot applicable — nothing published moves; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)— PR #18327 / #18180改了什么:开轮互斥要求每个新席读三份章程在 验证:机制在构造的 40 提交仓库上以两种深度确立(边界对象仍点名父提交但本地没有,git 拿它与空树做 diff,所以整棵树的每个路径都"在此被碰过";卡上建议的 要你做的:审 PR #18327,同意即 APPROVE;之后由本席按裁定 C 落地。 顺带发现(已立卡,不阻塞): Generated by Claude Code |
Fixes #18180
What
On a shallow clone,
git log -1 REF -- PATHnames the graft boundary as the last touch of a path the boundary never changed. The open-round mutex reading in.claude/skills/pm-dispatch/SKILL.md(the three charter files' latest-touch sha onorigin/main) named no instrument, so every seat took it with exactly that command on a fresh container clone — and read a real, plausible, wrong sha at exit 0 with no warning.scripts/pm/git-history.mjsgrows atouchmode:node scripts/pm/git-history.mjs touch --path=PATH [--ref=origin/main] [--format=%H] [--deepen=N] [--no-fetch] [--no-unshallow]. It prints the last-touch sha only when it can PROVE it — every parent the commit object names is present locally, and the diff against those parents touches the path — otherwise it deepens withgit fetch --deepen=N(counted from the current boundary, so it can only add history; N doubles from 64), then--unshallow, then REFUSES with exit 2 and empty stdout, the tool's standing contract. A one-line method receipt goes to stderr.git-history.mjs touch。」--statleg, refusal with empty stdout, deepen-then-answer, an answer while still shallow, the deepen that lands the boundary exactly on the true touch, a firing control, the real-root case, the complete-clone path,--format, and the usage errors. Self-test: 37 cases before → 54 after, exit 0.Mechanism — established on a constructed repo, not inherited
A 40-commit repo;
charter.mdlast touched at c2;f.txttouched by every commit.git log -1 origin/main -- charter.mdgit show --stat SHA -- charter.md--depth=5--depth=20The boundary commit's OBJECT still names its parent; the graft hides that parent from traversal, so git diffs the boundary against the empty tree. Every path in its tree reads as "added here", the pathspec walk stops, and
-1prints the boundary. Two depths, two different shas, one mechanism. Consequence for the verification leg the card proposed:git show --stat SHA -- PATHbeing non-empty does NOT discriminate the boundary — the same empty-tree diff prints the whole file as an insertion (33 lines for lanes/skills.md on the shared checkout today). The leg that discriminates is "the parent the object names is present locally"; only then is the diff a reading of the path.On the
b3b43b6question: the round-open marker recordsb3b43b6as theorigin/mainTIP at fetch time, not as a touch answer — its SKILL.md reading was8c657f7, which did touch SKILL.md (11 insertions, 11 deletions). There is no second mechanism.Reverse verification — on a genuine depth-50 clone of this repository
Clone: GitHub,
--depth=50, floorea0b24a(2026-09-14T16:27Z), tipa46cd8c.BEFORE — the unpatched spelling,
git log -1 --format='%h %cI' origin/main -- PATH:53dd5aa 2026-09-15T14:54:13— parent present, stat2 +-— true8c657f7 2026-09-14T23:24:17— parent present, stat2 +-— trueea0b24a 2026-09-14T16:27:40— the boundary; the parent it names (fbeb6d6) is absent; stat prints33 ++++(the whole file) — FALSE, and newer than the truthAFTER —
node scripts/pm/git-history.mjs touch --path=PATHon that clone:53dd5aaaafbe877a1cc72aef26511bd15c927ecf, exit 0,proved without fetching8c657f7dd0740e37e836edf14207ad9ce7836ec3, exit 0,proved without fetching9489e2c05a04173d6a0567fa9edd0f356a868781, exit 0 afterfetch --deepen=64thenfetch --deepen=128(clone 50 → 242 commits, still shallow); receiptproof: 1 parent(s) present locally, diff-tree touches .claude/skills/pm-dispatch/references/lanes/skills.md (4 ++--)Agreement: REST
GET /repos/objectstack-ai/objectstack/commits?sha=main&path=PATH&per_page=1(HTTP 200, no auth header) names53dd5aaaa/8c657f7dd/9489e2c05— byte-equal to the tool; rawgit log -1on the deepened clone names the same three.REFUSAL — a second fresh depth-50 clone,
touch --path=lanes/skills.md --no-fetch: exit 2, stdout 0 bytes; stderr:ea0b24a43 is a shallow graft boundary — its object names parent fbeb6d68e, which this clone does not have, so git diffed it against the EMPTY tree …, thenraw git log -1 said: ea0b24a43 (NOT a reading of the path)and a--deepenremedy.FIRING CONTROL — same clone,
touch --path=SKILL.md --no-fetch --format='%h %cI': exit 0,53dd5aa 2026-09-15T14:54:13+00:00, and no fetch happened (still 50 commits). The check discriminates rather than always refusing.Design choice — (B) a tool spelling, on the four axes
git-history.mjs("answer, or REFUSE" — AGENTS.md); a provenance reading in the same tool is the same principle, no workaround, and the refusal contract (empty stdout, exit 2) is the one already in force.$(...)gets an empty string, the refusal is loud and names the mechanism, and the self-test carries a firing control so "always refuses" cannot pass as "safe". Declaration = enforcement.Recommendation: (B), which is this PR.
Gates
All run locally at
d977c7a5f4b6b11d2e01976e19d2f931e8b99515.node scripts/pm/dispatch-gates.mjs --commandsderived 43 commands (the path-derived and the path-less derivations are identical).node scripts/pm/dispatch-gates.mjs --ran: ✓ 43 derived — 43 run, 0 NOT-MEASURED, 0 UNRUN (the doc-formula gate first exited 3 for a missing build; formula and lint were built under the verify lock and it re-ran at exit 0)node scripts/check-engine-split-ratio.mjs --days 90exit 2 is the gate's own refusal on this shallow worktree (floor 2026-09-14 inside the 90-day window): NOT MEASURED locally; lint.yml runs it on afetch-depth: 0checkoutnode scripts/pm/git-history.mjs --self-test: 37 cases → 54,all cases passed, exit 0 before and afterpnpm check:pm-skill-ratchet✓ SKILL.md 812 lines (ceiling 812), widest table row 342 (pin 342);pnpm check:pm-skill-id-lint✓ 27 files cleanpnpm exec eslint scripts/pm/git-history.mjsexit 0;node scripts/check-self-test-wired.mjs✓ 212/212;pnpm check:nul-bytes✓; a control-byte grep over both files: 0 hitsnode scripts/pm/check-governed-merges.mjs --test .claude/skills/pm-dispatch/SKILL.md scripts/pm/git-history.mjs→ exit 3, GOVERNED (.claude/**×1;scripts/pm/git-history.mjsnot on the register)3327d02c56f8a0eca88569dad2270f32unchanged;references/core-rules.md:19–:20 untouchedAcceptance notes
check-harness-current·latest touch·graft boundary·git log -1·touchIsProvable):scripts/pm/check-harness-current.mjstakesgit log -1 REF -- PATHon the shared shallow checkout and prints the graft boundary as a harness path's latest touch — on the shared checkout today.claude/hooks/*: latest touch 9b00f9f9cf, the boundary, whose true parent is absent. Its CURRENT/STALE verdict holds by construction (a boundary is under HEAD, and so is everything below it), but the printed sha is not a reading of the path. Remedy shape:touchIsProvable()from this PR, printingboundaryinstead of a sha. Not in this PR's file surface.references/platform-readings.mdcarries no row for this trap (the card proposed one); the rule now names the tool and the tool's header carries the measurement, so a row would be a second copy. Bearer: the skills seat on its next platform-readings fold.origin/mainadvanced by one commit (a46cd8c,check-half-states.mjs) after this branch was cut from82b3032c; it touches neither file here..claude/**,scripts/pm/**);skip-changesetapplied throughlabel-write.mjs.维护者速读(草稿)
改了什么:
scripts/pm/git-history.mjs新增touch子命令 — 在浅 clone 上证明「某文件在origin/main的最新触碰 sha」,证明不了就拒答(exit 2、stdout 空);SKILL.md:89 原位改一行(117 字节),让开轮互斥读数走这个工具。为什么改:每个新容器都是浅 clone,
git log -1 -- 文件会把 clone 的截断边界当成该文件的最新触碰打印出来 — 真实存在的 sha、exit 0、无警告。本席开轮标记里 lanes/skills.md 的读数就是这样错的,卡上 devx 席的读数也是;互斥检查建立在这个读数之上。风险与代价(含回滚):工具在需要时对本地 clone 做
fetch --deepen(只加历史、不减);回滚 = revert 本 PR 的一个 commit,两文件复原,无发布面。席位意见:(留空)
你要做的:审阅本 PR;
.claude/**属规则层,需授权 APPROVED 后由席位落地。Generated by Claude Code