Commit 0cc5ce8
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
| 89 | + | |
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| |||
0 commit comments