From 48a269256d81ba51a1eb8dd415d283e65167307c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 14:22:37 +0000 Subject: [PATCH 1/2] docs(pm-dispatch): re-key SKILL.md:92 from a re-seat prescription to a seat-post reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stale harness path on the shared checkout is noted on the seat post and picked up at the seat's next natural shift boundary; it no longer closes the shift or interrupts a batch. Line-neutral (812/812), 119 bytes, :91 unchanged. No twin in references/core-rules.md (grep for check-harness-current, 收班、换新会话, 不推进共享检出: zero hits there). Claude-Session: https://claude.ai/code/session_01Wj1HUjzyeiBQ8atRf1ZhaL Co-authored-by: Claude --- .claude/skills/pm-dispatch/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 7fb10228e37..bb9c0348c7e 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -89,7 +89,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 互斥清 ⇒ fetch 后三章程(本文、core-rules、本席章程)最新触碰 sha 走 `git-history.mjs touch`。 - 异于上一开轮标记即先重读;留开轮标记(session ID + fire 时刻 + 该触碰,注明重读)再跑轮。 - 同读 harness 载入面 `.claude/{settings.json,agents/*.md,hooks/*}` 的最新触碰是否已在共享检出 HEAD。 -- 否 ⇒ 收班、换新会话再派,⛔ 不推进共享检出;读数走 `scripts/pm/check-harness-current.mjs`。 +- 否 ⇒ 记座位贴,自然收班时换会话,⛔ 不中断批次;读数走 `scripts/pm/check-harness-current.mjs`。 ## 全体座位的不变量 From 6f0089211bb4afb96c726b12312da5296db60be4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 14:23:22 +0000 Subject: [PATCH 2/2] fix(pm): check-harness-current reports a stale harness path instead of prescribing a re-seat The STALE verdict now says what SKILL.md:92 says: note it on the seat post and pick it up at the seat's next natural shift boundary; never interrupt a batch for it. Exit codes 0/1/2 and the line-terminal `-- STALE` / `-- UNDECIDED` markers are unchanged; the CURRENT and UNDECIDED strings are unchanged. The header's load-moment claim is narrowed to what was measured (`.claude/settings.json`); the load moment of `.claude/hooks/*` and `.claude/agents/*.md` is stated as unmeasured, and the sentence naming re-seating as the remedy is gone. The self-test's STALE expectation is re-keyed to the new wording byte for byte, one case is added pinning that the summary names neither a re-seat nor advancing the shared checkout, and the case floor rises 14 -> 15 to cover it. Claude-Session: https://claude.ai/code/session_01Wj1HUjzyeiBQ8atRf1ZhaL Co-authored-by: Claude --- scripts/pm/check-harness-current.mjs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/scripts/pm/check-harness-current.mjs b/scripts/pm/check-harness-current.mjs index 5ed1baf1629..616f53f6db1 100644 --- a/scripts/pm/check-harness-current.mjs +++ b/scripts/pm/check-harness-current.mjs @@ -8,12 +8,15 @@ * node scripts/pm/check-harness-current.mjs [--shared ] [--ref origin/main] * node scripts/pm/check-harness-current.mjs --self-test * - * The harness reads `.claude/settings.json`, `.claude/agents/*.md` and `.claude/hooks/*` from - * the PRIMARY checkout when a session starts and never reloads them, so a touch that lands on - * `origin/main` after that clone is inert for the running session -- measured: a session whose - * primary checkout predated the MCP-write deny list still carried every denied tool. Worktrees - * do not help (the harness never reads them) and the primary checkout is never advanced in place - * (worktree-first), so the remedy is to close the shift and re-seat in a fresh session. + * Measured for `.claude/settings.json` ONLY: the harness reads it from the PRIMARY checkout when + * a session starts and does not reload it, so a deny-roster change that lands on `origin/main` + * after that clone is inert for the running session -- a session whose primary checkout predated + * the MCP-write deny list still carried every denied tool. The load moment of `.claude/hooks/*` + * and `.claude/agents/*.md` is UNMEASURED: whether the harness reads them once at start or from + * disk at each use is not known, and this tool asserts neither. Worktrees do not help (the + * harness never reads them) and the primary checkout is not advanced in place (worktree-first). + * A STALE verdict is a REPORT, not a prescription: the seat notes it on its seat post and picks + * it up at its next natural shift boundary; ⛔ it never interrupts a batch. * * Reads git only -- fetch first. Exit 0 = current; 1 = stale (each stale path printed with its * touch); 2 = undecidable: the ancestry test is negative on a SHALLOW clone and the touch is not @@ -104,7 +107,7 @@ function main() { console.log(`? ${path}: touch ${prov} is not under HEAD in a SHALLOW clone and not newer than HEAD -- UNDECIDED, deepen and rerun`); } const verdict = stale - ? `STALE -- ${stale} harness-loaded path(s) on ${ref} are not in ${shared} HEAD ${short(head.out)}: close the shift and re-seat in a fresh session; ⛔ never advance the shared checkout in place` + ? `STALE -- ${stale} harness-loaded path(s) on ${ref} are not in ${shared} HEAD ${short(head.out)}: note it on the seat post and pick it up at the seat's next natural shift boundary; ⛔ never interrupt a batch for it` : undecided ? `UNDECIDED -- ${undecided} path(s) could not be placed; fetch/deepen ${shared} and rerun` : `CURRENT -- every harness-loaded path on ${ref} is in ${shared} HEAD ${short(head.out)}`; console.log(`check-harness-current: ${verdict}`); return stale ? 1 : undecided ? 2 : 0; @@ -118,7 +121,7 @@ function main() { * inside the fixture block leaves the same silence. Adding cases is ordinary work; a run BELOW * this floor means cases stopped running, and the floor names that rather than passing. */ -const SELF_TEST_CASE_FLOOR = 14; +const SELF_TEST_CASE_FLOOR = 15; const FIXTURE_EPOCH = '2026-06-01T12:00:00Z'; const FIXTURE_COMMITS = 40; @@ -241,7 +244,14 @@ function selfTest() { // TWO paths, not one: HEAD sits at c34, below both the agents touch (c38) and the settings // touch (c39); only the hook touch (c1) is still under it. t('and its summary line is the STALE one, byte for byte', - summaryOf(behindRun.stdout) === `check-harness-current: STALE -- 2 harness-loaded path(s) on origin/main are not in ${behind} HEAD ${g(['rev-parse', 'HEAD'], behind).slice(0, 10)}: close the shift and re-seat in a fresh session; ⛔ never advance the shared checkout in place`, + summaryOf(behindRun.stdout) === `check-harness-current: STALE -- 2 harness-loaded path(s) on origin/main are not in ${behind} HEAD ${g(['rev-parse', 'HEAD'], behind).slice(0, 10)}: note it on the seat post and pick it up at the seat's next natural shift boundary; ⛔ never interrupt a batch for it`, + summaryOf(behindRun.stdout)); + // The verdict is a REPORT: it names the seat post and the shift boundary, and it names neither + // re-seating nor advancing the shared checkout -- the prescription that used to sit there. + t('and that summary is a report, not a prescription: it names the seat post and the shift ' + + 'boundary, and names neither a re-seat nor advancing the shared checkout', + /seat post/.test(summaryOf(behindRun.stdout)) && /shift boundary/.test(summaryOf(behindRun.stdout)) + && !/re-seat|fresh session|advance the shared checkout/.test(summaryOf(behindRun.stdout)), summaryOf(behindRun.stdout)); // An unreadable path stays exactly what it is today: UNDECIDED, exit 2, no provenance clause