Skip to content

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

Draft
claude[bot] wants to merge 1 commit into
mainfrom
claude/issue-18180-charter-touch-sha-shallow
Draft

claude[bot] wants to merge 1 commit into
mainfrom
claude/issue-18180-charter-touch-sha-shallow

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

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 fbeb6d68e, which this clone does not have, so git diffed it against the EMPTY tree …, then raw git log -1 said: ea0b24a43 (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 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, 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 [finding] check-plugin-teardown-shape --self-test exits 1 (a real-defect code) when a shallow clone cannot reach its pinned control commit — should be exit 3 = PREREQUISITE NOT MET; measured on two independent dispatches the same afternoon #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

…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
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d977c7a5f4b6b11d2e01976e19d2f931e8b99515

Reviewing seat: domain:skills, in-seat review at the contract-review tier — get_session read at 2026-09-15T07:15Z: configured_model = session_context.model = last_served_model, the constant's tier, no fallback notice this round. Card #18180, claim 5685167419, report 5685767439 (posted through the tool; read back clean); Clause-②: no on the claim; the dev built at the review tier by the SKILL.md path mandate; seat review at tier.

① Derived judgments

  • Accept set / public surface: none moves — .claude/skills/pm-dispatch/SKILL.md :89 (one line, in place, 119 → 117 B; 812 / 812) and scripts/pm/git-history.mjs (+364/−9) against merge base 82b3032c (the branch does not carry a46cd8c4, which touches neither file — not rebased, per the rules); nothing published; skip-changeset read back (documentation + size/m + skip-changeset). GOVERNED (.claude/** ×1 by check-governed-merges --test, exit 3) ⇒ the four-piece hangs here and the PR waits for an authorized APPROVED; ruling C after it.
  • The change, read whole on a detached worktree of the head: design (B), the seat's reading confirmed on the four axes — git-history.mjs gains a touch mode (touch --path=<file> [--ref] [--format] [--deepen] [--no-fetch] [--no-unshallow]) whose proof is two pure reads before any sha is printed: every parent the commit OBJECT names is present locally (objectParents via cat-file -p / cat-file -e; a graft boundary fails here, a real root names none and passes) and diff-tree against those parents touches the path (touchIsProvable); unprovable ⇒ fetch --deepen=N doubling from 64 (additive by definition), then --unshallow, then REFUSE with exit 2 and EMPTY stdout (ensureTouchProvable / touchMain, the tool's existing contract); the header gains the measured section 「the second trap, same root」; the self-test gains a 17-case battery (roster floor 3 → 4). SKILL.md :89 now reads 「互斥清 ⇒ fetch 后三章程(本文、core-rules、本席章程)最新触碰 sha 走 git-history.mjs touch。」 — no issue number, no new line, the frame block :734–:755 md5 3327d02c56f8a0eca88569dad2270f32 unchanged. Seat re-runs on the head: git-history --self-test all cases pass (37 → 54), ESLint 0/0, check:pm-skill-ratchet / check:pm-skill-id-lint / check:skill-frame-sync / check-self-test-wired all 0, --pair 18327 0. Live probes by the seat on the shared shallow checkout with --no-fetch: lanes/skills.md REFUSED — 「9b00f9f9c is a shallow graft boundary — its object names parent 82d8942, which this clone does not have」, exit 2, stdout empty; SKILL.md answered 53dd5aaa without fetching (the firing control); REST GET /commits?sha=main&path=…&per_page=1 reads 9489e2c05 / 53dd5aaaa — the tool's refusal and answer agree with the depth-independent channel.
  • Gates 43 / 43 / 0 by --ran on the head (check-engine-split-ratio --days 90 recorded at its own exit 2 — the gate's shallow-clone refusal on the shared clone, measured by CI at fetch-depth 0; not rewritten to 3); the battery 1730 pass (dev); the seat's own battery run on its worktree is reported in the ACCEPT thread. Reverse verification: the mechanism ESTABLISHED on a constructed 40-commit repo at depths 5 and 20 (the boundary's object still names a parent the clone lacks; git diffs it against the empty tree; both depths name their own boundary — one mechanism), and on a real depth-50 clone (lanes/skills.md → boundary ea0b24a before; the tool → 9489e2c05 after two deepen steps, still shallow, byte-equal to REST and to a deepened clone; a fresh clone refuses with exit 2 / empty stdout; the firing control answers 53dd5aa without a fetch).

② Semver level

Not applicable — nothing published moves; skip-changeset measured.

③ Boundary flags

open_questions: none. Two seat premises REFUTED and owned here — (1) 「a boundary commit prints nothing for git show --stat <sha> -- <path>」 is false: the boundary prints the WHOLE file as an insertion against the empty tree, so the card's proposed --stat verification leg is itself fooled (the seat's zero was a grep for the full path against a stat line that abbreviates it); (2) 「b3b43b6 was answered for SKILL.md while the clone was 50 deep」 is false: the marker recorded b3b43b6 as the tip and 8c657f7 as SKILL.md's touch, which is true — the grading comment on the card was corrected in place at 2026-09-15T18:29Z; the defect stands on the boundary reading (9b00f9f for lanes/skills.md). One dispatch spelling wrong (scripts/pm/check-self-test-wired.mjs does not exist; the dev ran scripts/check-self-test-wired.mjs, 212 / 212) — owned. Seven deviations answered: the wired path — accepted; the --stat premise — accepted, the tool's proof is parent presence + diff-tree; the b3b43b6 premise — accepted; check-engine-split-ratio NOT MEASURED locally, the shared clone not deepened by the dev — accepted (an operator action; CI measures); the draft PR opened after the local gates — accepted; origin/main advanced by one unrelated commit, not rebased — accepted; the formula + lint builds under os-verify-lock to make check:doc-formula-expressions measure (exit 0) — accepted. Out of scope: one card FILED by the seat, #18330 (check-harness-current.mjs prints the boundary as a harness path's touch; p3 Task, this lane, serial behind this PR); noted, not filed: a platform-readings row for the trap would be a second copy now that :89 names the tool; #18217 (devx) stays its own taker's. Landing: governed — the four-piece hangs here (this record, the 速读终稿 below, the ACCEPT on #18180, needs-user-decision, reviewers os-zhuang + hotlong requested); ⛔ this seat writes no approving review; ruling C after an authorized APPROVED — record on the current head, --pair 0, checks green, clear needs-user-decision, ready through the CCR route, auto-merge SQUASH. #18308 (SKILL.md :629) stays SERIAL behind this PR.

Implemented-by: claude/issue-18180-charter-touch-sha-shallow
Reviewed-by: session_01HZfg2AwVX191qCizp88gQr

VERDICT: PASS


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)— PR #18327 / #18180

改了什么:开轮互斥要求每个新席读三份章程在 origin/main 的「最新触碰 sha」,而容器克隆是浅克隆,git log -1 -- <path> 在浅克隆上会把 graft 边界当成该文件的最后一次触碰——返回一个真实、像样、却从未碰过该文件的 sha,exit 0,无任何警告。本席自己的开轮标记就中招了(lanes/skills.md 记成了边界 9b00f9f)。修法:scripts/pm/git-history.mjs 新增 touch 子命令,只在两点都成立时才打印 sha——提交对象点名的每个父提交在本地都存在(边界在此失败),且对这些父提交的 diff 确实碰到该路径;不成立就 fetch --deepen(64 起倍增)、再 --unshallow、仍不行则 exit 2 且 stdout 为空(与该工具原有契约一致)。SKILL.md :89 原地改为「…最新触碰 sha 走 git-history.mjs touch」(119 → 117 字节,812/812 不变)。

验证:机制在构造的 40 提交仓库上以两种深度确立(边界对象仍点名父提交但本地没有,git 拿它与空树做 diff,所以整棵树的每个路径都"在此被碰过";卡上建议的 git show --stat 校验腿同样被这个空树 diff 骗过);真实 depth-50 克隆上工具答出 9489e2c05,与 REST GET /commits?path= 及加深克隆逐字节一致;本席在共享浅检出上实测:lanes/skills.md 拒答(exit 2,stdout 空,点名边界与缺失父提交),SKILL.md 不 fetch 即答 53dd5aaa(发火对照)。自检 37 → 54 例;43 条派生门禁全部记录退出码(1 条是该门禁自身在浅克隆上的拒答,CI 以 fetch-depth 0 实测);电池 1730 例通过;ratchet / id-lint / frame-sync 绿;--pair 0。

要你做的:审 PR #18327,同意即 APPROVE;之后由本席按裁定 C 落地。⚠️ 本席自己在定级时写错了两条证据(边界的 --stat 并非空;b3b43b6 是当时的 tip 而非 SKILL.md 的读数),已在卡上原地更正并在记录里认领;缺陷本身成立。

顺带发现(已立卡,不阻塞):check-harness-current.mjs 在共享浅检出上把边界 sha 当作 harness 路径的最新触碰打印出来;CURRENT/STALE 判定本身不受影响,只有那行出处是假的。串行于本 PR 之后,用同一条证明腿修。


Generated by Claude Code

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 needs-user-decision size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant