Repository navigation
feat(hooks): SessionStart hook prints the pm-dispatch pointer, its SKILL.md touch sha and the lane’s seat-post line - #25386
Conversation
…uch sha and the lane's seat-post line A multi-repo project session has the parent of the checkouts as its working directory, so a nested checkout's .claude/skills never reaches the session's skill list and /pm-dispatch cannot be invoked there. session-start-fleet.sh prints the pointer to SKILL.md into the session's first context, the latest touch of SKILL.md on HEAD (a shallow clone's graft boundary is labelled as one, never presented as a measured touch), and — when OS_FLEET_LANE names a lane — the lane's seat-post number read from references/lanes/<lane>.md plus the stub reminder. No network, no GitHub read, exit 0 always; located by its own path, never the cwd. Registered beside the PreToolUse guards; its self-test builds a hermetic fixture checkout. seat-lifecycle.md gains one line in 〈开轮互斥〉 naming the hook, paid by the reader-routing line the SKILL.md stage index already carries. Claude-Session: https://claude.ai/code/session_01SSWEKCxzY8HYr8EsFpR8VG Co-authored-by: Claude <noreply@anthropic.com>
…o the repo-wide gate The selftest's own construct scan spelled the forbidden tokens into a grep pattern and a message, which scripts/check-bash32-floor.mjs flags wherever they stand (no filename allowlist, by design). The floor is already held over .claude/hooks/** by that gate; the selftest keeps bash -n and the executable bit and points at the gate instead. Claude-Session: https://claude.ai/code/session_01SSWEKCxzY8HYr8EsFpR8VG Co-authored-by: Claude <noreply@anthropic.com>
Verification (head e75a6b5)
First sweep (head 857b804) had two reds, both resolved before this head: Generated by Claude Code |
Contract reviewServed-tier: Reviewed 2026-10-23T05:28Z. Inputs: card #25380 body and both comments (the claim and the os-dev-report), PR #25386 body, file list and net diff against merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对ACCEPT · Landing authority: the maintainer's own words in the seat thread of the Claude Code project "Objectstack" (
Generated by Claude Code |
Requested by Elon · project thread
Fixes #25380
Clause-②: no
Metadata-protocol: none
维护者速读(草稿)
SessionStart钩子.claude/hooks/session-start-fleet.sh,会话一开始就把三行话印进上下文:① 「先读.claude/skills/pm-dispatch/SKILL.md与阶段 references 再派发/认领/复核/落地」的指针;② 本检出上 SKILL.md 最近一次触碰的 sha(供席位与开轮标记比对;浅克隆边界会被明确标出、不冒充真触碰);③ 车道行:环境里有OS_FLEET_LANE就印该车道的席位贴号与「席位贴只是存根,姿态与台账住项目里」的提醒,没有就印lane: not set。配套 selftest(58 例)、settings.json注册一条、seat-lifecycle.md加一行点名钩子(删一行已由 SKILL.md 索引承载的读者路由句付账,96→95 行,上限不动)。.claude/skills进不了技能列表,两天来每条席位线程都是靠项目指令手读 SKILL.md。规则仍只住仓库里一份,钩子只印指针。set -u中断都被 trap 收成 0),失败最多少印几行,不会挡会话;每次会话开始多 3–5 行上下文。一次 revert 可回(删钩子两文件 + settings 一段 + seat-lifecycle 一行)。两处实测要您知道:(a) 本卡线程里CLAUDE_PROJECT_DIR未设、已有的 PreToolUse 守卫对一条已知应拦的命令没有拦——即 objectstack 的settings.json在这种「父目录开会话」的线程里很可能根本没被加载(或$CLAUDE_PROJECT_DIR指向父目录),那样本钩子在项目线程里同样不会触发;单仓会话与 CI 自检不受影响。(b)references/lanes/*.md目前没有任何一份写了席位贴号,钩子如实印「not recorded」;补号是各车道文件各加一行Seat post: #N,不在本卡范围。settings.json(见 A1)。What changed
.claude/hooks/session-start-fleet.sh(new)SessionStarthook. Prints the pointer line, the SKILL.md path and its latest touch on HEAD, an optional one-line note whenCLAUDE_PROJECT_DIRis not this checkout, and the lane line. Dependency-free bash (3.2 floor), no jq, no network, no GitHub read, no write;trap 'exit 0' EXITso it exits 0 under every input measured (closed/empty/malformed stdin, no git on PATH, no checkout, no SKILL.md, a refused lane value, an-uabort). Everything is resolved from the hook's own path, never the cwd..claude/hooks/session-start-fleet.selftest.sh(new)Seat post: #19287, one without): pointer verbatim, touch sha =git log -1 -- SKILL.md, shallow-boundary labelling (the fixture's touch commit written into the git dir'sshallowfile flips the wording and removing it flips it back), lane set / unset / empty / no file / no number / path characters / shell metacharacters, cwd andCLAUDE_PROJECT_DIRnot being inputs, ≤15 lines,bash -nand the executable bit (the bash-3.2 floor itself ischeck:bash32-floor's, repo-wide), thesettings.jsonwiring (registered onSessionStart, sibling spelling, not also aPreToolUseguard), and a NON-VACUITY tail (a mutated pointer, anexit 1copy and aset -uabort are each caught or held). Discovered by the existingfind .claude/hooks -name '*.selftest.sh'step inlint.yml, so CI runs it with no workflow edit..claude/settings.jsonhooks.SessionStartentry:"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start-fleet.sh", the sibling spelling.permissionsuntouched (check:pm-settings-deny-rostergreen)..claude/skills/pm-dispatch/references/seat-lifecycle.md- \session-start-fleet.sh` 钩子开会话即印 SKILL.md 触碰 sha 与车道席位贴行,供比对开轮标记。` (115 bytes). −1 line: the opening reader-routing sentence 「每个座位每次 fire 读本文:…」, whose content the SKILL.md 〈阶段细则索引〉 row for this file already carries (reader + both read moments). 96 → 95 lines; the ceiling row stays 96 (headroom 1, no ratchet edit). Placed away from L69 (PR #25348) and from 〈座位贴协议〉 (#25379). ⛔ Not a second copy of the pointer text.The exact lines the hook prints
On this branch's checkout (HEAD
e75a6b53, a shallow clone), noOS_FLEET_LANE:With
OS_FLEET_LANE=skills, the last line becomes:On a full clone the touch line reads
touch: SHA — latest commit touching SKILL.md on HEAD SHA; compare with the opening mark (seat-lifecycle.md 〈开轮互斥〉) before the round.; with a lane file carryingSeat post: #19287the lane line readslane: skills · seat post #19287 (.claude/skills/pm-dispatch/references/lanes/skills.md); the seat post is a stub; posture and ledger live in the project.(both pinned by the self-test). WhenCLAUDE_PROJECT_DIRis set and is not this checkout, one more line is printed:note: this session's project dir is not this checkout, so the harness's skill list does not include this checkout's .claude/skills — hence this pointer.The shallow-boundary wording is not decoration:
8010b64is in this clone's.git/shallow, andgit log -1 -- SKILL.mdnames it only because the graft makes the boundary look like it touched every path — the exact fabricationcheck-harness-current.mjs's header documents. Onorigin/mainfrom a full checkout the real touch is older; the hook refuses to present the boundary as a reading.Mechanism assumptions, measured (dispatch A1–A4)
objectstack/.claude/settings.jsonfire in a project thread, and what does it see? Measured from this card thread (working directory/home/claudeat start, the parent of the three checkouts):CLAUDE_PROJECT_DIRis unset in the Bash tool's environment, and the registered PreToolUse guardguard-tree-enum.shdid not blockfor f in .github/workflows/*.yml; do git show "origin/main:$f" | head -1; done— its own self-test's first measured block case — the command ran and printed. So in this thread shape either the nested checkout'ssettings.jsonis not loaded at all, or$CLAUDE_PROJECT_DIRresolves to the parent, where$CLAUDE_PROJECT_DIR/.claude/hooks/…does not exist and the hook command fails non-blocking. This contradicts the card's premise that the guards fire in card threads; it may differ by how a thread is rooted. Consequence: the hook is correct and self-tested, but whether a project thread ever runs it is a platform question, not something this PR can settle. The hook locates SKILL.md from its own path, so wherever it IS run it prints the right checkout. A registration that also looks under$CLAUDE_PROJECT_DIR/objectstack/…would not help if the file is not loaded, so the sibling spelling is kept rather than guessing.settings.jsonfrom the primary checkout at session start and does not reload it, and the measured thread does not run the registered hooks at all (A1). ⛔ Not claimed. The self-test evidence and the exact lines above stand in; the first post-merge project thread that shows the pointer in its first context is the acceptance reading, recorded by the seat.seat-lifecycle.mdplacement. One line in 〈开轮互斥〉 (L21 after the edit), away from L69 (docs(pm-dispatch): revoke every autonomous-adjudication channel — a seat analyses and recommends, every needs_decision goes to the maintainer's ruling batch #25348, which only rewords that line and moves no ceiling) and from 〈座位贴协议〉 (pm-dispatch: a project-run seat form — seat post as a stub, posture and ledger in the Claude Code project, dispatch order in the card thread, branch named before the thread opens; 项目总监 → 总监席 (maintainer's word 2026-10-23) #25379).origin/mainmerged when the seat says so.OS_FLEET_LANEspelling.git grepoverscripts/,.claude/,.github/: no script reads a lane name from the environment under any spelling (OS_LANE_STEPS/OS_LANE_JOB_STATUSinlint.ymlare the CI lane-verdict inputs, a different thing).OS_FLEET_LANEis new and collides with nothing; it follows theOS_FLEET_*family (fleet-token.mjs,fleet-write/**).references/lanes/*.mdcarry no#Nat all (grep -rn '19287\|6023\|1353' .claude/is empty); seat posts are found by thepm:seatlabel index. The hook reads an optional lineSeat post: #Nfrom the lane file as the card specifies and saysnot recordedwhen absent. Writing those lines is one line per lane file, outside this card's file surface — carrier: the skills seat (a follow-up card or a rider on pm-dispatch: a project-run seat form — seat post as a stub, posture and ledger in the Claude Code project, dispatch order in the card thread, branch named before the thread opens; 项目总监 → 总监席 (maintainer's word 2026-10-23) #25379's reference edit).Verification
bash .claude/hooks/session-start-fleet.selftest.sh→58 passed, 0 failed, exit 0 (log in the PR's verification comment). Hook exits 0 with and withoutOS_FLEET_LANE, on a checkout with no SKILL.md, in a directory that is no checkout, with no git on PATH, with closed, empty and malformed stdin.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon the actual changed set (4 paths, 392 changed lines, under the 3000 threshold): 19 commands derived (--commands, no paths — the tool took the change set off the merge base itself), every one run on heade75a6b53with its exit code captured before any pipe and reconciled with--ran:19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED, all exit 0. The list: check-closing-keyword-parity (+ --self-test), check-comment-mask-corpus, check-harness-current --self-test, lintcheck:doc-formula-expressions(exit 3 PREREQUISITE NOT MET on the first sweep until@objectstack/formulaand@objectstack/lintwere built; exit 0 on the final sweep), check:agent-test-spelling, check:bash32-floor (red on the first sweep — the selftest had spelled the forbidden tokens into a grep pattern; fixed in the second commit, green since), check:cross-package-test-inputs, check:doc-authoring, check:driver-memory-census, check:gitlink-declared, check:nul-bytes, check:pm-governed-merges, check:pm-settings-deny-roster, check:pm-skill-id-lint, check:pm-skill-ratchet, check:refd-timer-probe, check:skill-frame-sync, check:watch-hint-literal. Repo-wide scans (pnpm lint, the hook self-test discovery step, the type-check lanes) are CI's; this PR's check-runs are the reading for those.pnpm check:pm-skill-ratchet: green —seat-lifecycle.md95 lines against its ceiling 96 (headroom 1; the ceiling row is not moved), every edited line ≤120 bytes (the new line is 115), no cross-file move declared.check:pm-skill-id-lintcovers the skill files; the hook is read bycheck:doc-authoringandcheck:bash32-floor, both in the derived list).Acceptance notes
references/lanes/*.mdcarry noSeat post: #Nline (A5) — carrier: skills seat; noted, not filed.CLAUDE_PROJECT_DIRunset in the tool environment — a platform reading for the seat'splatform-readings.mdfact table (reference edits are outside this card). Carrier: skills seat; noted, not filed.Governed surface
.claude/**(Tier S): draft, awaiting the seat'sCONTRACT_REVIEW_TIERreview and the maintainer's word; ⛔ not to be flipped ready, queued or armed by this thread.skip-changeset: no published package is touched (.claude/**and a reference file only).🤖 Generated with Claude Code
https://claude.ai/code/session_01SSWEKCxzY8HYr8EsFpR8VG