Skip to content

Commit 0cc5ce8

Browse files
claude[bot]claude
andauthored
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)
Fixes #18180 ## What 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 open-round mutex reading in `.claude/skills/pm-dispatch/SKILL.md` (the three charter files' latest-touch sha on `origin/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.mjs` grows a `touch` mode: `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 with `git 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. - SKILL.md line 89 is re-keyed in place (119 → 117 bytes; the file stays at 812/812 lines) to name the instrument: 「互斥清 ⇒ fetch 后三章程(本文、core-rules、本席章程)最新触碰 sha 走 `git-history.mjs touch`。」 - A new self-test battery of 17 cases pins the defect (baseline), the mechanism, the fooled `--stat` leg, 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.md` last touched at c2; `f.txt` touched by every commit. | clone | `git log -1 origin/main -- charter.md` | parent named by the commit object | that parent present locally | `git show --stat SHA -- charter.md` | |---|---|---|---|---| | `--depth=5` | c35 — the graft boundary | yes | no | non-empty: charter.md, 1 insertion | | `--depth=20` | c20 — the graft boundary | yes | no | non-empty: charter.md, 1 insertion | | deepened | c2 — correct | yes | yes | charter.md, 1 insertion, 1 deletion | The 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 `-1` prints the boundary. Two depths, two different shas, one mechanism. Consequence for the verification leg the card proposed: `git show --stat SHA -- PATH` being 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 `b3b43b6` question: the round-open marker records `b3b43b6` as the `origin/main` TIP at fetch time, not as a touch answer — its SKILL.md reading was `8c657f7`, 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`, floor `ea0b24a` (2026-09-14T16:27Z), tip `a46cd8c`. BEFORE — the unpatched spelling, `git log -1 --format='%h %cI' origin/main -- PATH`: - SKILL.md → `53dd5aa 2026-09-15T14:54:13` — parent present, stat `2 +-` — true - core-rules.md → `8c657f7 2026-09-14T23:24:17` — parent present, stat `2 +-` — true - lanes/skills.md → `ea0b24a 2026-09-14T16:27:40` — the boundary; the parent it names (`fbeb6d6`) is absent; stat prints `33 ++++` (the whole file) — FALSE, and newer than the truth AFTER — `node scripts/pm/git-history.mjs touch --path=PATH` on that clone: - SKILL.md → `53dd5aaaafbe877a1cc72aef26511bd15c927ecf`, exit 0, `proved without fetching` - core-rules.md → `8c657f7dd0740e37e836edf14207ad9ce7836ec3`, exit 0, `proved without fetching` - lanes/skills.md → `9489e2c05a04173d6a0567fa9edd0f356a868781`, exit 0 after `fetch --deepen=64` then `fetch --deepen=128` (clone 50 → 242 commits, still shallow); receipt `proof: 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) names `53dd5aaaa` / `8c657f7dd` / `9489e2c05` — byte-equal to the tool; raw `git log -1` on 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 fbeb6d6, which this clone does not have, so git diffed it against the EMPTY tree …`, then `raw git log -1 said: ea0b24a (NOT a reading of the path)` and a `--deepen` remedy. 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 - **实际业务需求**: the reading is real and consumed — three files per round, every round, by the mutex comparison; it was measured wrong on two seats this shift (devx on 2026-09-14, skills on 2026-09-15). (A) prose would give the seat a hand-run two-step whose verification leg is itself fooled by the boundary; (B) is one command. - **项目长远合理性**: windowed history questions already go through `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. - **防 AI 写代码犯错**: (A) leaves a spelling that looks right and depends on the seat noticing; (B) makes the wrong reading impossible to capture — `$(...)` 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. - **创业阶段不扩散需求**: one subcommand on an existing tool, +17 self-test cases, no new file, no new gate, no ceiling moved; SKILL.md loses two bytes. No staged or dual spelling — the old spelling is replaced, not aliased. Recommendation: (B), which is this PR. ## Gates All run locally at `d977c7a5f4b6b11d2e01976e19d2f931e8b99515`. `node scripts/pm/dispatch-gates.mjs --commands` derived 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) - 42 of 43 exit 0. `node scripts/check-engine-split-ratio.mjs --days 90` exit 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 a `fetch-depth: 0` checkout - `node scripts/pm/git-history.mjs --self-test`: 37 cases → 54, `all cases passed`, exit 0 before and after - `pnpm check:pm-skill-ratchet` ✓ SKILL.md 812 lines (ceiling 812), widest table row 342 (pin 342); `pnpm check:pm-skill-id-lint` ✓ 27 files clean - `pnpm exec eslint scripts/pm/git-history.mjs` exit 0; `node scripts/check-self-test-wired.mjs` ✓ 212/212; `pnpm check:nul-bytes` ✓; a control-byte grep over both files: 0 hits - `node 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.mjs` not on the register) - Frame block SKILL.md :734–:755 md5 `3327d02c56f8a0eca88569dad2270f32` unchanged; `references/core-rules.md` :19–:20 untouched ## Acceptance notes - To file (class a, dedupe: `check-harness-current` · `latest touch` · `graft boundary` · `git log -1` · `touchIsProvable`): `scripts/pm/check-harness-current.mjs` takes `git log -1 REF -- PATH` on 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 9b00f9f`, 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, printing `boundary` instead of a sha. Not in this PR's file surface. - Noted, not filed: `references/platform-readings.md` carries 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. - Card #18217 (a self-test that refuses on a shallow clone) is a different symptom of the same depth; it is not addressed here and remains open. - `origin/main` advanced by one commit (`a46cd8c`, `check-half-states.mjs`) after this branch was cut from `82b3032c`; it touches neither file here. - Changeset: none — nothing published moves (`.claude/**`, `scripts/pm/**`); `skip-changeset` applied through `label-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](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent fae612a commit 0cc5ce8

2 files changed

Lines changed: 365 additions & 10 deletions

File tree

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
8686
- 收班简报是前任不再写的显式声明,是释放标记不是锁:简报即最新事件 ⇒ 立即坐席。
8787
- 滞后标题是进场顺手修的半状态,⛔ 不是阻塞;简报点名的留守尾巴作围栏。
8888
- 维护者明示召唤是仲裁:有简报径直坐席;无简报才走保守确认,确认终止即坐席。
89-
- 互斥清 ⇒ fetch 后读三章程文件(本文、core-rules、本席章程)`origin/main` 的最新触碰 sha
89+
- 互斥清 ⇒ fetch 后三章程(本文、core-rules、本席章程)最新触碰 sha 走 `git-history.mjs touch`
9090
- 异于上一开轮标记即先重读;留开轮标记(session ID + fire 时刻 + 该触碰,注明重读)再跑轮。
9191
- 同读 harness 载入面 `.claude/{settings.json,agents/*.md,hooks/*}` 的最新触碰是否已在共享检出 HEAD。
9292
- 否 ⇒ 收班、换新会话再派,⛔ 不推进共享检出;读数走 `scripts/pm/check-harness-current.mjs`

0 commit comments

Comments
 (0)