Skip to content

Commit c663245

Browse files
docs(pm-dispatch): pin the shared checkout to a detached origin/main before a subagent batch (#24685)
Fixes #24585 Clause-②: no ## What changed One section of `.claude/skills/pm-dispatch/references/dispatch-runbook.md`, the pre-dispatch step for a subagent batch (lines 137-141). The PM seat no longer fast-forwards its session branch on the shared checkout. It fetches `main` and checks out `origin/main` detached: ```text git -C REPO-ROOT fetch origin main && git -C REPO-ROOT checkout --detach origin/main ``` (`REPO-ROOT` stands for the file's existing placeholder spelling, which is unchanged in the file.) - L137, the heading: "先快进本地检出" becomes "先把本地检出钉到 main". - L139: the old 139 and the reason half of the old 141 merge into one line. Subagents read agent definitions and skill text from the local checkout, so a stale checkout gives the whole batch stale definitions. - L140: the command above replaces `pull --ff-only origin main`. - L141: states the properties the new form actually has. On a detached HEAD the stop hook skips its branch block, and the session branch is never fast-forwarded. Only dirty changes that conflict with the switch are refused, and divergence is not refused. This replaces the old promise that `--ff-only` makes "a dirty or diverged checkout fail loudly". The measurements below show that half of that promise was never true for dirty changes, and that the new form drops the divergence half. Ruling of record: the maintainer's 「技能侧可选改法,立skills卡片」, recorded verbatim in the card body. The carded option is implemented exactly as written. The two items in the card's "Not in this card" section are untouched: the hook's own counting, and the stale local ref. ## Line budget - File: 241 lines before, 241 after. The ceiling is 241 (`check:pm-skill-ratchet`: "is 241 lines (ceiling 241; headroom 0)"). - Lines changed: 4 replaced in place. Nothing was re-wrapped to make room. - Bytes per changed line, without the newline, against `MAX_LINE_BYTES = 120`: L137 49 to 57; L139 100 to 113; L140 87 to 111; L141 117 to 120. - The dispatch's premise quoted L140 and L141 as 88 and 118 bytes. Those figures include the newline. The gate counts without it, so its numbers are 87 and 117. ## The three measurements the card asks for All readings were taken on `origin/main` `eadea5720` (this branch's base). The git-behaviour probe ran in a throwaway repository under the session scratchpad: a bare origin, an author clone, and a "shared" clone on a session branch whose `origin/SESSION-BRANCH` ref was written locally and never pushed. The probe never touched the shared checkout, and it was deleted before this PR. The stop hook script itself was read only: it was not executed and not modified. The probe ran the same git commands the hook runs. ### 1. Every reader of the shared checkout's branch name `git grep -nE "abbrev-ref|show-current|symbolic-ref|symbolic-full-name|@\{u\}|..." HEAD -- .claude/ scripts/pm/ .githooks/` returns 11 hits. Three of them read a branch name. The others are prose about detached processes or clones. | Reader | What it does with the name | On a detached HEAD | |---|---|---| | `.claude/hooks/guard-main-checkout.sh:120` | `rev-parse --abbrev-ref HEAD`, printed in the refusal text at L132 only. Whether to block is decided structurally (git-dir vs common-dir, L114-117). | Prints `HEAD` (probe E). The block decision is unchanged. | | `.claude/hooks/guard-main-checkout-bash.sh` | Reads no branch name: zero hits. Same structural git-dir vs common-dir test (L347-350). | Unaffected. | | `scripts/pm/os-regen-merge.sh:891` (`mode_run`) | Refuses `main` and `HEAD`. | Refuses to run on the shared checkout, with exit 1. That is stricter than today: on the session branch it would proceed. `:729` writes the name into its record only after `mode_run` passed, so only in a dev worktree. | | `scripts/pm/check-harness-current.mjs` | Reads `rev-parse HEAD` as a sha (L124, L139, L141, L157) and no name. | Unaffected. | | `.githooks/pre-commit`, `.githooks/pre-push` | Zero branch-name reads. The seat never commits or pushes from the shared checkout. | Unaffected. | ### 2. Other fast-forwards in the skill or `os-dev.md` that assume a branch `git grep -nE "git pull|pull --|快进|fast-forward|--ff\b|ff-only|共享检出|shared checkout|本地检出|primary checkout" HEAD -- .claude/skills/pm-dispatch/ .claude/agents/os-dev.md` returns 11 hits. The only fast-forward is the one this PR replaces (old L140-141). The rest are: - `SKILL.md:31` and `os-dev.md:28` say never edit the shared checkout. Compatible. - `reading-discipline.md:8` says do not verify `main` from the shared checkout's tree. Compatible. - `seat-lifecycle.md:21` is the harness-current reading, which works by sha. Compatible. - `platform-readings.md:59` uses 快进 for squash committer dates. Unrelated. Outside the skill, `scripts/check-skill-frame-freshness.mjs:136` prints the remedy `git fetch origin main && git merge --ff-only origin/main`. It runs in whatever checkout invokes it; `os-dev.md:151` lists it as a dev-worktree local family. Probe D: on a detached HEAD, that merge fast-forwards HEAD with exit 0, and `branch --show-current` stays empty, so it is compatible. ### 3. The stop hook's control flow, and whether it stays silent across rounds Source: `/root/.claude/stop-hook-git-check.sh`, md5 `d36da5d989abb71904a379c7de566b95`, read only. - L6-24: recursion guard, not a git repo, or no remote, then exit 0. - L27-37: uncommitted or untracked changes, then exit 2. This runs before any branch read, so it still fires on a dirty detached checkout. That is correct, and this change does not alter it. - L39-40: `current_branch=$(git branch --show-current)`, then `if [[ -n "$current_branch" ]]`. Everything else lives inside that block: the signing check (L77-114) and the `$upstream..HEAD` count (L116-124). - L127: exit 0. - Reading: on a detached HEAD, `git branch --show-current` prints an empty string. The whole branch block is skipped and the hook exits 0. The card's claim is **confirmed**. Probe readings, using the hook's own commands: | Step | `branch --show-current` | Hook branch block | `rev-list origin/BRANCH..HEAD` | `HEAD ^origin/main` | |---|---|---|---|---| | A1, old recipe, after 3 commits land on main | `claude/sess` | entered | **3** (the reported symptom) | 0 | | A2, new recipe, round 1 | empty | skipped | n/a | 0 | | A3, new recipe, round 2 | empty | skipped | n/a | 0 | | A4, new recipe, round 3 | empty | skipped | n/a | 0 | After A2 the session branch ref is still present and unmoved. **NOT MEASURED:** a live seat session's stop hook across several real dispatch rounds. That needs this text landed and a seat running it, and the hook belongs to the harness, which this run must not execute. The seat can confirm it on its first rounds after landing. Live reading of the shared checkout at this PR's open (read only): it is on `claude/eloquent-curie-6slepw` at `3a931fca1`. Its remote branch exists at the same sha, a zero-diff mirror of `main` pushed by the seat. This is the cost the card names. `origin/main` is already 2 commits ahead, so the next fast-forward under the old text would show 2 "unpushed" commits. ### The refusal surface, old vs new (probe B and C) | Case | `pull --ff-only` (old) | `checkout --detach origin/main` (new) | |---|---|---| | B1: dirty tracked file the incoming commits do not touch | exit 0, change carried along | exit 0, change carried along (`M b.txt`) | | B2: dirty tracked file the incoming commits touch | exit 1, "would be overwritten by merge" | exit 1, "would be overwritten by checkout" | | B3: untracked file colliding with an incoming file | not run | exit 1, "untracked working tree files would be overwritten" | | C1: local commit on the session branch | exit 128, "Not possible to fast-forward" | exit 0; the commit stays on the session branch | | C2: local commit on the detached HEAD | not applicable | exit 0 with "you are leaving 1 commit behind"; the commit is reachable from no ref | So the old line's "dirty fails loudly" held only for B2 under either form. Its "diverged fails loudly" is lost: C1 and C2 both exit 0. The new L141 says this ("不拒分叉") instead of carrying the old promise forward. Divergence on the shared checkout already breaks the never-edit rule (`SKILL.md:31`, plus the two PreToolUse guards). In C1 the commit survives on its branch; in C2 it survives only in the reflog. ## Gates All 17 derived families exit 0 at `ee7b8181e` (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, change set derived from git: 1 path, +4/-4). Each exit code was captured before any pipe: - `node scripts/check-closing-keyword-parity.mjs`: 0 - `node scripts/check-closing-keyword-parity.mjs --self-test`: 0 - `node scripts/check-comment-mask-corpus.mjs`: 0 - `node scripts/pm/check-harness-current.mjs --self-test`: 0 - `pnpm --filter @objectstack/lint run check:doc-formula-expressions`: the first attempt exited 3 (PREREQUISITE NOT MET: formula and lint not built), so nothing was measured. After `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under the verify lock, it exits 0. - `pnpm check:agent-test-spelling`: 0 - `pnpm check:cross-package-test-inputs`: 0 - `pnpm check:doc-authoring`: 0 - `pnpm check:driver-memory-census`: 0 - `pnpm check:gitlink-declared`: 0 - `pnpm check:nul-bytes`: 0 - `pnpm check:pm-governed-merges`: 0 - `pnpm check:pm-skill-id-lint`: 0 ("87 file(s) clean") - `pnpm check:pm-skill-ratchet`: 0 ("dispatch-runbook.md is 241 lines (ceiling 241; headroom 0)"; the 120-byte line check prints only on failure) - `pnpm check:refd-timer-probe`: 0 - `pnpm check:skill-frame-sync`: 0 - `pnpm check:watch-hint-literal`: 0 Reconciliation with `--ran` (each line annotated with its exit code): "17 derived, 17 run, 0 NOT-MEASURED, 0 UNRUN", and "a DERIVED zero — all 17 recorded an exit code and none of them is 3". No package is touched, so there is no dependency-closure build and no package test or typecheck. `.claude/**` publishes nothing, so this PR carries `skip-changeset`. ## Acceptance notes - The header of `scripts/pm/check-harness-current.mjs` says "the primary checkout is not advanced in place (worktree-first)". The runbook has advanced it in place before every batch, both before this PR (fast-forward) and after it (detached checkout). This is comment drift and nothing reads it. Carrier: none. - `os-regen-merge.sh` will now refuse on the shared checkout (`HEAD`), where before it would have run on the session branch. This is a side effect in the safe direction, noted only. - One instance of the card's cost, taken from the dispatch as an example and not as a ruling: the environment pre-created `origin/claude/eloquent-curie-6slepw`, and the seat pushed the shared checkout there twice as a zero-diff mirror of `main`. ## 维护者速读(草稿) **改了什么** —— PM 席位每批派发子代理前,不再在会话分支上快进本地检出,改为取回 main 后以游离 HEAD 钉在 `origin/main`。只改技能手册的一节,4 行原位替换,行数不变。 **为什么改** —— 云端停止钩子按会话分支计未推送提交。快进让 main 的提交挂到会话分支上,钩子每轮提示推送。照做会推出与 main 零差异的分支,本席位已推过两次。游离 HEAD 下钩子跳过分支检查,提示消失。子代理读到的仍是最新定义。 **风险与代价(含回滚)** —— 新写法不再拒绝分叉,共享检出上若有本地提交会被丢下或留在原分支。但共享检出本就禁止编辑,有两道钩子把守。与切换冲突的脏改动仍被拒。回滚即还原这 4 行。 **席位意见** —— **你要做的** —— --- _Generated by [Claude Code](https://claude.ai/code/session_01HEdP5tPhqQfzwRmKDxwn85)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3b8e2b1 commit c663245

1 file changed

Lines changed: 4 additions & 4 deletions

File tree

‎.claude/skills/pm-dispatch/references/dispatch-runbook.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,11 @@
134134
- 该答案与从不存在不可区分,⛔ 永不读作死亡信号。
135135
- 跨账号接班活性只认 GitHub 产出读数,draft-PR 时点交报正为此存在。
136136

137-
## subagent 批派发前置:先快进本地检出
137+
## subagent 批派发前置:先把本地检出钉到 main
138138

139-
- subagent 在 PM 容器内运行,agent 定义与技能文本读本地检出,不是 `origin/main`。
140-
- 派发 subagent 批之前先快进:`git -C <repo-root> pull --ff-only origin main`。
141-
- 检出过期则整批拿到过期定义;用 `--ff-only` 让脏或分叉的检出大声失败,⛔ 不静默合并。
139+
- subagent 读 PM 容器本地检出的 agent 定义与技能文本,不读 `origin/main`:过期则整批过期。
140+
- 每批派发前:`git -C <repo-root> fetch origin main && git -C <repo-root> checkout --detach origin/main`。
141+
- 游离 HEAD 使停止钩子跳过分支块,⛔ 不快进会话分支;只拒与切换冲突的脏改动,不拒分叉。
142142

143143
## 接手中断的 dev(worktree 接手协议)
144144

0 commit comments

Comments
 (0)