session-orchestrator 5.12.0 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +162 -4
  11. package/README.md +126 -127
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +1 -1
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +90 -17
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +49 -15
  47. package/docs/session-config-template.md +30 -22
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +139 -49
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -2
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +39 -27
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/section-extractor.mjs +65 -11
  143. package/scripts/lib/config/template-keys.mjs +153 -0
  144. package/scripts/lib/config/vault-integration.mjs +1 -0
  145. package/scripts/lib/config.mjs +2 -2
  146. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  147. package/scripts/lib/discovery/triage-state.mjs +2 -1
  148. package/scripts/lib/dispatcher/rank.mjs +5 -7
  149. package/scripts/lib/ecosystem-health.mjs +2 -2
  150. package/scripts/lib/eval/engine.mjs +3 -2
  151. package/scripts/lib/eval/session-resolve.mjs +6 -2
  152. package/scripts/lib/eval/sink.mjs +4 -3
  153. package/scripts/lib/events-retention-banner.mjs +3 -2
  154. package/scripts/lib/events-schema.mjs +10 -0
  155. package/scripts/lib/events.mjs +23 -12
  156. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  157. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  158. package/scripts/lib/fleet-protocol.mjs +4 -0
  159. package/scripts/lib/gates/gate-full.mjs +172 -11
  160. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  161. package/scripts/lib/git-config-drift.mjs +141 -7
  162. package/scripts/lib/github-export.mjs +447 -0
  163. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  164. package/scripts/lib/handover-queue-marks.mjs +159 -0
  165. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  166. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  167. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  168. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  169. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  170. package/scripts/lib/host-identity.mjs +9 -5
  171. package/scripts/lib/http-request.mjs +64 -0
  172. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  173. package/scripts/lib/io.mjs +6 -3
  174. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  175. package/scripts/lib/issue-budget.mjs +36 -7
  176. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  177. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  178. package/scripts/lib/learnings/io.mjs +289 -15
  179. package/scripts/lib/learnings/select.mjs +53 -4
  180. package/scripts/lib/learnings/store-role.mjs +209 -0
  181. package/scripts/lib/learnings/surface.mjs +46 -7
  182. package/scripts/lib/ledger-path.mjs +113 -0
  183. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  184. package/scripts/lib/memory-banner.mjs +5 -6
  185. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  186. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  187. package/scripts/lib/memory-proposals/store.mjs +22 -5
  188. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  189. package/scripts/lib/no-follow-open.mjs +57 -0
  190. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  191. package/scripts/lib/own-lock-root.mjs +95 -0
  192. package/scripts/lib/owner-interview.mjs +2 -1
  193. package/scripts/lib/owner-yaml.mjs +70 -7
  194. package/scripts/lib/platform.mjs +161 -9
  195. package/scripts/lib/plugin-update-banner.mjs +15 -11
  196. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  197. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  198. package/scripts/lib/process-group.mjs +270 -34
  199. package/scripts/lib/project-hygiene.mjs +28 -0
  200. package/scripts/lib/quality-gate.mjs +33 -9
  201. package/scripts/lib/reconcile/backlog.mjs +29 -15
  202. package/scripts/lib/reconcile/engine.mjs +26 -2
  203. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  204. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  205. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  206. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  207. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  208. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  209. package/scripts/lib/rule-loader.mjs +20 -10
  210. package/scripts/lib/scope-baseline.mjs +123 -8
  211. package/scripts/lib/scope-echo.mjs +5 -2
  212. package/scripts/lib/scope-gate.mjs +55 -20
  213. package/scripts/lib/session-close-backfill.mjs +75 -7
  214. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  215. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  216. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  217. package/scripts/lib/session-id.mjs +9 -1
  218. package/scripts/lib/session-identity/own-session.mjs +63 -0
  219. package/scripts/lib/session-lock.mjs +114 -0
  220. package/scripts/lib/session-record-repair.mjs +245 -35
  221. package/scripts/lib/session-registry.mjs +72 -9
  222. package/scripts/lib/session-schema/constants.mjs +9 -0
  223. package/scripts/lib/session-schema/filters.mjs +56 -1
  224. package/scripts/lib/session-schema/validator.mjs +15 -5
  225. package/scripts/lib/session-start-probes.mjs +149 -22
  226. package/scripts/lib/session-token-rollup.mjs +2 -1
  227. package/scripts/lib/session-work-trees.mjs +166 -0
  228. package/scripts/lib/sessions-canonical.mjs +61 -3
  229. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  230. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  231. package/scripts/lib/sh-quote.mjs +1 -0
  232. package/scripts/lib/skill-evidence-window.mjs +5 -1
  233. package/scripts/lib/skill-health/join.mjs +10 -9
  234. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  235. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  236. package/scripts/lib/snapshot-probe.mjs +414 -0
  237. package/scripts/lib/so-hud/collect.mjs +49 -5
  238. package/scripts/lib/so-hud/runtime.mjs +15 -4
  239. package/scripts/lib/spiral-carryover.mjs +8 -1
  240. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  241. package/scripts/lib/state-md/idle-reset.mjs +14 -5
  242. package/scripts/lib/state-md/mission-status.mjs +5 -1
  243. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  244. package/scripts/lib/state-md.mjs +31 -8
  245. package/scripts/lib/sunset/walker.mjs +4 -3
  246. package/scripts/lib/telemetry/consent.mjs +4 -2
  247. package/scripts/lib/telemetry/queue.mjs +3 -3
  248. package/scripts/lib/telemetry/sync.mjs +45 -9
  249. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  250. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  251. package/scripts/lib/ux-grill/paths.mjs +5 -1
  252. package/scripts/lib/validate/check-agents.mjs +37 -6
  253. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  254. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  255. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  256. package/scripts/lib/validate/check-json-files.mjs +3 -1
  257. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  258. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  259. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  260. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  261. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  262. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  263. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  264. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  265. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  266. package/scripts/lib/validate/confidential-names.mjs +3 -1
  267. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  268. package/scripts/lib/vault-mirror/namespace.mjs +19 -7
  269. package/scripts/lib/vault-mirror/process.mjs +56 -27
  270. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  271. package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
  272. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  273. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  274. package/scripts/lib/vault-mirror/utils.mjs +79 -0
  275. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  276. package/scripts/lib/vault-status/board-writer.mjs +2 -1
  277. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  278. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  279. package/scripts/lib/win-process-tree.mjs +137 -0
  280. package/scripts/lib/workspace.mjs +1 -1
  281. package/scripts/lib/worktree/constants.mjs +1 -1
  282. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  283. package/scripts/lib/worktree/listing.mjs +1 -1
  284. package/scripts/lib/zx.mjs +50 -0
  285. package/scripts/mcp-server.mjs +16 -7
  286. package/scripts/mcp-server.sh +13 -83
  287. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  288. package/scripts/memory-propose.mjs +10 -3
  289. package/scripts/migrate-cold-start-seed.mjs +7 -0
  290. package/scripts/migrate-legacy-learnings.sh +22 -0
  291. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  292. package/scripts/migrate-vault-paths.mjs +29 -13
  293. package/scripts/parse-config.mjs +71 -14
  294. package/scripts/print-learnings-index.mjs +25 -2
  295. package/scripts/release-session-lock.mjs +94 -7
  296. package/scripts/release.mjs +451 -24
  297. package/scripts/relocate-vault-corpus.mjs +11 -7
  298. package/scripts/repair-invalid-sessions.mjs +68 -3
  299. package/scripts/resolve-session-invocation.mjs +10 -17
  300. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  301. package/scripts/run-quality-gate.mjs +33 -7
  302. package/scripts/self-update.mjs +56 -7
  303. package/scripts/session-commit-files.mjs +133 -0
  304. package/scripts/site-numbers.mjs +52 -9
  305. package/scripts/snapshot-probe.mjs +156 -0
  306. package/scripts/so-hud.mjs +29 -6
  307. package/scripts/state-md-idle-reset.mjs +2 -1
  308. package/scripts/sweep-expired-learnings.mjs +54 -6
  309. package/scripts/sweep-expired-rules.mjs +4 -1
  310. package/scripts/validate-plugin.mjs +22 -0
  311. package/scripts/vault-integration-watcher.mjs +54 -6
  312. package/scripts/vault-mirror.mjs +9 -0
  313. package/skills/_shared/fleet-protocol.md +142 -1
  314. package/skills/_shared/instruction-file-resolution.md +1 -1
  315. package/skills/_shared/monitor-patterns.md +44 -25
  316. package/skills/_shared/platform-tools.md +7 -0
  317. package/skills/_shared/state-ownership.md +3 -2
  318. package/skills/architecture/references/domain-model.md +1 -1
  319. package/skills/bootstrap/SKILL.md +3 -1
  320. package/skills/bootstrap/_shared-template.md +4 -4
  321. package/skills/bootstrap/fast-template.md +1 -1
  322. package/skills/bootstrap/private-contract.md +1 -1
  323. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  324. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  325. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  326. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  327. package/skills/claude-md-drift-check/SKILL.md +3 -3
  328. package/skills/claude-md-drift-check/checker.mjs +61 -101
  329. package/skills/close/SKILL.md +20 -7
  330. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  331. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  332. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  333. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  334. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  335. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  336. package/skills/discovery/probes-session.md +3 -0
  337. package/skills/discovery/probes-vault.md +2 -0
  338. package/skills/dispatcher/SKILL.md +1 -1
  339. package/skills/eval/SKILL.md +3 -1
  340. package/skills/evolve/SKILL.md +2 -0
  341. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  342. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  343. package/skills/gitlab-ops/SKILL.md +7 -6
  344. package/skills/gitlab-portfolio/SKILL.md +2 -2
  345. package/skills/go/SKILL.md +13 -1
  346. package/skills/npm-publish/SKILL.md +5 -2
  347. package/skills/plan/SKILL.md +23 -10
  348. package/skills/plan/mode-feature.md +13 -8
  349. package/skills/plan/mode-new.md +6 -14
  350. package/skills/plan/prd-feature-template.md +19 -0
  351. package/skills/plan/prd-full-template.md +12 -5
  352. package/skills/plan/prd-premortem-panel.md +48 -0
  353. package/skills/plan/prd-reviewer-prompt.md +11 -3
  354. package/skills/plan/soul.md +1 -1
  355. package/skills/portfolio/SKILL.md +5 -6
  356. package/skills/quality-gates/SKILL.md +2 -4
  357. package/skills/reconcile/SKILL.md +27 -4
  358. package/skills/release/SKILL.md +16 -8
  359. package/skills/session-end/SKILL.md +94 -11
  360. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  361. package/skills/session-end/phase-3-6-tail.md +11 -7
  362. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  363. package/skills/session-end/plan-verification.md +16 -1
  364. package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
  365. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  366. package/skills/session-end/references/session-summary-template.md +4 -0
  367. package/skills/session-end/session-metrics-write.md +7 -4
  368. package/skills/session-plan/SKILL.md +5 -3
  369. package/skills/session-start/SKILL.md +9 -3
  370. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  371. package/skills/session-start/phase-8-5-express-path.md +4 -3
  372. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  373. package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
  374. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  375. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  376. package/skills/tmux-layout/SKILL.md +2 -2
  377. package/skills/vault-sync/validator.mjs +3 -2
  378. package/skills/wave-executor/SKILL.md +5 -4
  379. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  380. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  381. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  382. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  383. package/assets/og-card.png +0 -0
  384. package/assets/og-card.svg +0 -47
  385. package/assets/wave-lifecycle.svg +0 -98
@@ -33,19 +33,22 @@ import { existsSync, writeSync } from 'node:fs';
33
33
  import { execFile } from 'node:child_process';
34
34
  import { promisify } from 'node:util';
35
35
  import { randomUUID } from 'node:crypto';
36
+ import { hostname } from 'node:os';
36
37
  import path from 'node:path';
37
38
 
38
39
  import { shouldRunHook } from './_lib/profile-gate.mjs';
39
40
  import { ensureOrchestratorIgnore } from './_lib/orchestrator-ignore.mjs';
41
+ import { resolveSessionStartRoot } from './_lib/hook-root.mjs';
40
42
 
41
43
  import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
42
44
  import { readNavigatorLease, fleetIntegrationEnabled } from '../scripts/lib/fleet-protocol.mjs';
43
45
  import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
44
46
  import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
45
- import { getPlatform, resolveProjectDir } from '../scripts/lib/platform.mjs';
47
+ import { getPlatform, resolveProjectDir, resolveSessionRoot } from '../scripts/lib/platform.mjs';
46
48
  import {
47
49
  registerSelf,
48
50
  detectPeers,
51
+ countRecentStarts,
49
52
  sweepZombies,
50
53
  logSweepEvent,
51
54
  repoPathHash,
@@ -58,8 +61,9 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
58
61
  // SSOT for the "is this a re-entry into the same logical session?" question
59
62
  // (#1091). Defined in the lock-bootstrap leaf module, which this hook already
60
63
  // loads, so the preservation branch below and the lock force-refresh gate can
61
- // never drift apart.
62
- import { SAME_LOGICAL_SESSION_SOURCES } from './_lib/lock-bootstrap.mjs';
64
+ // never drift apart. readClaudePid() is the process witness current-session.json
65
+ // carries next to the raw id (#1746).
66
+ import { SAME_LOGICAL_SESSION_SOURCES, readClaudePid } from './_lib/lock-bootstrap.mjs';
63
67
 
64
68
  const execFileAsync = promisify(execFile);
65
69
 
@@ -187,14 +191,20 @@ function supersessionMarker(peer) {
187
191
  }
188
192
 
189
193
  /**
190
- * Queue the ONE `additionalContext` string for the single end-of-hook flush.
191
- * Last writer wins; there is deliberately no accumulation, because every
192
- * character here costs context on every session start.
194
+ * Queue text for the ONE `additionalContext` string of the single end-of-hook
195
+ * flush. Two writers exist (the consent nudge, the #1635 lead notice) and both
196
+ * are needed by the model, so a second text is appended on its own line; an
197
+ * identical text is not repeated. Every character costs context on every
198
+ * session start, so writers stay terse and conditional.
193
199
  * @param {string|null|undefined} text
194
200
  */
195
201
  function setAdditionalContext(text) {
196
202
  if (typeof text !== 'string' || text.length === 0) return;
197
- pendingAdditionalContext = text;
203
+ if (typeof pendingAdditionalContext !== 'string' || pendingAdditionalContext.length === 0) {
204
+ pendingAdditionalContext = text;
205
+ } else if (!pendingAdditionalContext.split('\n').includes(text)) {
206
+ pendingAdditionalContext = `${pendingAdditionalContext}\n${text}`;
207
+ }
198
208
  }
199
209
 
200
210
  /**
@@ -241,14 +251,16 @@ function flushBanner() {
241
251
  // ---------------------------------------------------------------------------
242
252
 
243
253
  /**
244
- * Run a git command in cwd; return trimmed stdout. Returns null on failure.
254
+ * Run a git command in cwd; return trimmed stdout. Returns null on failure
255
+ * (including a timeout, when `timeoutMs` is given).
245
256
  * @param {string[]} args
246
257
  * @param {string} cwd
258
+ * @param {number} [timeoutMs] — kill the git child after this many ms (0 = no limit)
247
259
  * @returns {Promise<string|null>}
248
260
  */
249
- async function gitOutput(args, cwd) {
261
+ async function gitOutput(args, cwd, timeoutMs = 0) {
250
262
  try {
251
- const { stdout } = await execFileAsync('git', args, { cwd });
263
+ const { stdout } = await execFileAsync('git', args, { cwd, timeout: timeoutMs });
252
264
  return stdout.trim();
253
265
  } catch {
254
266
  return null;
@@ -316,25 +328,27 @@ export async function readColdStartConfig(projectRoot) {
316
328
  }
317
329
 
318
330
  /**
319
- * Read `resource-thresholds.concurrent-sessions-warn` from Session Config.
320
- * Used to decide when the peer banner should show the WARN icon. Defaults to
321
- * 5 — matches the documented default in docs/session-config-reference.md.
331
+ * Start-wave rule (#1650): WARN when at least START_WAVE_MIN foreign registry
332
+ * entries started within the last START_WAVE_WINDOW_MIN minutes.
333
+ *
334
+ * Why not `peer_count >= concurrent-sessions-warn` (the previous WARN trigger):
335
+ * that is a permanent state, not an event. Measured 2026-10-17 08:08Z/08:09Z
336
+ * over `orchestrator.session.started` since 2026-08-24, dedup session_id +
337
+ * timestamp, per `host_class`: peer_count >= 5 fired on 880/1305 = 67.4% (M4)
338
+ * and 255/991 = 25.7% (M5) of starts — a broken instrument per HR-101.
339
+ * This rule, reconstructed from the start timestamps themselves, fires on
340
+ * 35/1305 = 2.7% (M4) and 52/991 = 5.2% (M5). Neighbouring candidates:
341
+ * >=3 in 2 min 4.0% / 7.1%, >=5 in 2 min 1.7% / 3.3%, >=3 in 5 min 6.3% / 10.7%.
342
+ * 5 min was chosen over 2 min so a wave spread over a few minutes still shows.
343
+ *
344
+ * Ceiling (BV-004): fixed constants, no config key; the reconstruction is an
345
+ * upper bound (sessions that ended inside the window leave the registry).
346
+ * Revisit when either host's firing rate on `recent_starts_count >= 5` in the
347
+ * event ledger exceeds 10%, or a start wave that hurt was not flagged.
348
+ * Report only — this single soft signal caps nothing (HR-104).
322
349
  */
323
- async function peerWarnThreshold(projectRoot) {
324
- const DEFAULT = 5;
325
- for (const name of ['CLAUDE.md', 'AGENTS.md']) {
326
- try {
327
- const raw = await readFile(path.join(projectRoot, name), 'utf8');
328
- // Match `concurrent-sessions-warn: <int>` inside resource-thresholds or top-level.
329
- const m = raw.match(/^\s*concurrent-sessions-warn:\s*(\d+)\b/im);
330
- if (m) {
331
- const n = parseInt(m[1], 10);
332
- if (!Number.isNaN(n) && n > 0) return n;
333
- }
334
- } catch { /* file missing is fine */ }
335
- }
336
- return DEFAULT;
337
- }
350
+ const START_WAVE_WINDOW_MIN = 5;
351
+ const START_WAVE_MIN = 5;
338
352
 
339
353
  /**
340
354
  * Non-blocking stdin read for Claude Code hook payload. Returns parsed JSON or
@@ -372,8 +386,13 @@ async function readStdinJson(timeoutMs = 500) {
372
386
  * Codex/Cursor may not).
373
387
  *
374
388
  * Best-effort: any persistence failure is swallowed (hook must remain non-blocking).
389
+ *
390
+ * @param {object|null} input — the parsed SessionStart payload.
391
+ * @param {string} projectRoot
392
+ * @param {number|null} [claudePid=null] — readClaudePid(); recorded with the
393
+ * host as the writer's process witness (#1746).
375
394
  */
376
- async function resolveSessionId(input, projectRoot) {
395
+ async function resolveSessionId(input, projectRoot, claudePid = null) {
377
396
  const nativeSource = typeof input?.source === 'string' && input.source.length > 0
378
397
  ? input.source
379
398
  : null;
@@ -477,9 +496,12 @@ async function resolveSessionId(input, projectRoot) {
477
496
  // (`resume` was ABSENT from hooks.json until #1091, so on Claude Code this
478
497
  // hook never ran on a native resume), `startup|reload|new|resume|fork` on
479
498
  // pi, and every source on Cursor (empty matcher). Several of those sources
480
- // are re-entries into the SAME logical session. On clear/compact the UUID
481
- // `session_id` changes while the `semantic_session_id`
482
- // (branch+date+mode+n) stays stable; whether Claude Code PRESERVES the raw
499
+ // are re-entries into the SAME logical session. The `semantic_session_id`
500
+ // (branch+date+mode+n) is meant to stay stable across them. The raw UUID
501
+ // `session_id` does not behave alike per source: on Claude Code `compact`
502
+ // KEEPS it (12/12 compact events in events.jsonl carry
503
+ // `resume_linkage: raw-id`, measured 2026-10-14), while `clear` showed
504
+ // 6/6 `resume_linkage: none`. Whether Claude Code PRESERVES the raw
483
505
  // `session_id` across a native resume is what #1091 measures via
484
506
  // `native_source` + `resume_linkage` on `orchestrator.session.started` —
485
507
  // nothing here ASSUMES either answer, it only reacts to the raw id it can
@@ -548,6 +570,11 @@ async function resolveSessionId(input, projectRoot) {
548
570
  pid: process.pid,
549
571
  source,
550
572
  timestamp: new Date().toISOString(),
573
+ // #1746 — `pid` above is this short-lived hook process, no witness of
574
+ // anything after it exits. The Claude Code process survives /clear, so
575
+ // the next run can tell its own predecessor from a peer that started
576
+ // later in this checkout. Absent without a readable CLAUDE_PID.
577
+ ...(typeof claudePid === 'number' ? { claude_pid: claudePid, host: hostname() } : {}),
551
578
  // Spread AFTER the base fields so the preserved high-water marks survive
552
579
  // (and never clobber the identity fields above).
553
580
  ...preserved,
@@ -565,12 +592,18 @@ async function resolveSessionId(input, projectRoot) {
565
592
  mode: normalizedMode,
566
593
  nativeSource,
567
594
  resumeLinkage,
568
- // The raw session_id our OWN previous run of this hook recorded, or null.
569
- // Surfaced (rather than kept local) because bootstrapLock() needs it as the
570
- // third conjunct of the #1091 F2 force-refresh — see the call site in main().
595
+ // The raw session_id the LAST session start in this checkout recorded, or
596
+ // null. Every session start rewrites the file, so it is our own previous
597
+ // run only when no peer started here since (#1746). Surfaced (rather than
598
+ // kept local) because bootstrapLock() needs it as the third conjunct of
599
+ // the #1091 F2 force-refresh — see the call site in main().
571
600
  predecessorSessionId: typeof prev?.session_id === 'string' && prev.session_id.length > 0
572
601
  ? prev.session_id
573
602
  : null,
603
+ // #1746 — the process witness that writer recorded, unvalidated: the
604
+ // consumer compares it with isSameClaudeProcess(), which admits only an
605
+ // exact pid match on this host.
606
+ predecessorProcess: prev ? { claude_pid: prev.claude_pid, host: prev.host } : null,
574
607
  };
575
608
  }
576
609
 
@@ -780,7 +813,14 @@ function maybeQueueConsentNudge() {
780
813
  // ---------------------------------------------------------------------------
781
814
 
782
815
  async function main() {
783
- const projectRoot = resolveProjectDir();
816
+ // #1605 — the root depends on the payload `source`/`cwd`/`session_id`, so the
817
+ // optional stdin payload is read BEFORE the first writer below. Best-effort:
818
+ // readStdinJson resolves null on a closed/empty/unparseable stdin and caps
819
+ // itself at 500 ms. The rule (entered worktree only for clear/compact/resume,
820
+ // only a verified linked worktree, never over another live session's state)
821
+ // lives in hooks/_lib/hook-root.mjs, beside the readers' own-lock lookup.
822
+ const input = await readStdinJson();
823
+ const projectRoot = await resolveSessionStartRoot(input, resolveProjectDir());
784
824
 
785
825
  // #1515 Pkt 5 — before this hook's own writers (host.json, session.lock,
786
826
  // events.jsonl) run: an un-bootstrapped repo gets `.orchestrator/.gitignore`
@@ -788,11 +828,6 @@ async function main() {
788
828
  // pay two existsSync and nothing else. Never throws.
789
829
  await ensureOrchestratorIgnore(projectRoot);
790
830
 
791
- // Read optional stdin payload in parallel with git info so we don't stall
792
- // the hook. Both are best-effort and the promise races against a 500 ms cap
793
- // inside readStdinJson itself.
794
- const stdinPromise = readStdinJson();
795
-
796
831
  // Resolve project name: basename of git toplevel, falling back to cwd basename.
797
832
  const topLevel = await gitOutput(['rev-parse', '--show-toplevel'], projectRoot);
798
833
  const projectName = topLevel
@@ -809,6 +844,24 @@ async function main() {
809
844
  const rawHeadSha = await gitOutput(['rev-parse', 'HEAD'], projectRoot);
810
845
  const headSha = rawHeadSha && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(rawHeadSha) ? rawHeadSha : null;
811
846
 
847
+ // #1668 — the upstream state at start, so emit-session can tell a stale
848
+ // head_sha (checkout behind its upstream: a later pull drags foreign commits
849
+ // into `<head_sha>..HEAD`) from a current one. Local refs only — no fetch, no
850
+ // network — and bounded at 1 s per call. No upstream configured, a timeout or
851
+ // a non-numeric count → BOTH keys omitted (not measured), never 0.
852
+ let upstreamSha = null;
853
+ let behind = null;
854
+ if (headSha) {
855
+ const [rawUpstream, rawBehind] = await Promise.all([
856
+ gitOutput(['rev-parse', '--verify', '--quiet', '@{u}^{commit}'], projectRoot, 1000),
857
+ gitOutput(['rev-list', '--count', 'HEAD..@{u}'], projectRoot, 1000),
858
+ ]);
859
+ if (rawUpstream && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(rawUpstream) && rawBehind && /^\d+$/.test(rawBehind)) {
860
+ upstreamSha = rawUpstream;
861
+ behind = Number(rawBehind);
862
+ }
863
+ }
864
+
812
865
  // v3.1.0 env-aware banner (opt-out via enable-host-banner: false in Session Config).
813
866
  // The ask-via-tool nudge rides the same opt-out flag — both are coordinator
814
867
  // reminders shown at session start; users who silence one expect silence.
@@ -872,13 +925,18 @@ async function main() {
872
925
 
873
926
  // v3.1.0 multi-session registry (#168). All steps best-effort — failures
874
927
  // must never break the hook, which is informational-only.
875
- const input = await stdinPromise;
876
928
  // #1091 — the native SessionStart `source` (Claude Code: startup|resume|
877
929
  // clear|compact), plus the linkage decision resolveSessionId() derived from
878
930
  // it (see SAME_LOGICAL_SESSION_SOURCES). Both are surfaced on the event so
879
931
  // #1091 gets its measurement instead of an inference.
880
- const { sessionId, semanticSessionId, mode, nativeSource, resumeLinkage, predecessorSessionId } =
881
- await resolveSessionId(input, projectRoot);
932
+ // #1641: the Claude Code process survives /clear while the raw id and the
933
+ // semantic label are minted anew — its pid is the witness that a live lock,
934
+ // or the current-session.json entry (#1746), is our own predecessor's.
935
+ // Admitted only as this hook's direct parent: a Codex child of a Claude
936
+ // session inherits the variable.
937
+ const claudePid = readClaudePid();
938
+ const { sessionId, semanticSessionId, mode, nativeSource, resumeLinkage, predecessorSessionId, predecessorProcess } =
939
+ await resolveSessionId(input, projectRoot, claudePid);
882
940
  // getPlatform() ALREADY implements the SO_PLATFORM override as step 1 of its
883
941
  // precedence — and, unlike a bare `??`, it validates the value against the
884
942
  // four-platform allowlist and trims it. The former `process.env.SO_PLATFORM ??
@@ -905,12 +963,23 @@ async function main() {
905
963
  mode,
906
964
  ttlHours: 4,
907
965
  nativeSource,
908
- // The raw session_id our OWN previous run of this hook recorded. It is the
966
+ // The raw session_id the last session start in this checkout recorded —
967
+ // our own previous run unless a peer started here since. It is the
909
968
  // third conjunct of the same-logical-session force-refresh (#1091 F2): a
910
969
  // semantic-label match alone is collidable on one host (#1066), so the
911
970
  // live lock must ALSO carry our predecessor's raw id before we take it
912
971
  // over. `prev` is read above, before the semantic id is minted.
913
972
  predecessorSessionId,
973
+ // #1746: who wrote that raw id — the lead yield trusts it as "our own
974
+ // predecessor" only when this same Claude process did.
975
+ predecessorProcess,
976
+ claudePid,
977
+ // #1635: a fleet lead holds this main checkout — no lock was taken; the
978
+ // model and the operator are told to work in a worktree instead.
979
+ onNotice: (context, banner) => {
980
+ setAdditionalContext(context);
981
+ pushBanner(banner);
982
+ },
914
983
  });
915
984
  } catch { /* hook must remain non-blocking */ }
916
985
 
@@ -973,6 +1042,12 @@ async function main() {
973
1042
  platform,
974
1043
  mode,
975
1044
  hostClass: bannerData?.host?.host_class ?? null,
1045
+ // #1605 — the pointer the readers (Stop, PostToolBatch, SessionEnd, the
1046
+ // current-session.json side writers) follow to THIS session's lock,
1047
+ // whatever their cwd; and the working copy, so a later relocation
1048
+ // into it by another session sees this one (B4). Stop refreshes it.
1049
+ sessionRoot: path.resolve(projectRoot),
1050
+ workRoot: path.resolve(resolveSessionRoot(input?.cwd)),
976
1051
  });
977
1052
  } catch (err) {
978
1053
  // Registration failed — emit an observability breadcrumb to sweep.log.
@@ -981,6 +1056,9 @@ async function main() {
981
1056
  }
982
1057
  peers = await detectPeers({ sessionId }).catch(() => []);
983
1058
  } catch { /* swallow — hook must remain non-blocking */ }
1059
+ // #1650 — foreign starts inside the window. detectPeers() already excludes
1060
+ // self, and its 15-min freshness covers the 5-min window (see helper JSDoc).
1061
+ const recentStarts = countRecentStarts(peers, { windowMin: START_WAVE_WINDOW_MIN });
984
1062
 
985
1063
  // Append a peer line to the host banner when a banner was already emitted
986
1064
  // and at least one peer is live on this host.
@@ -1004,10 +1082,11 @@ async function main() {
1004
1082
  selfRepoHash && p.repo_path_hash ? p.repo_path_hash === selfRepoHash : false);
1005
1083
  const elsewhere = peers.filter((p) => !sameCopy.includes(p));
1006
1084
 
1007
- const threshold = await peerWarnThreshold(projectRoot);
1008
1085
  // WARN on any peer in THIS working copy — one is already enough to collide
1009
- // — or on host-wide count crossing the configured threshold.
1010
- const icon = sameCopy.length > 0 || peers.length >= threshold ? '⚠️ ' : '';
1086
+ // — or on a start wave (#1650). The host-wide peer count stays on the line
1087
+ // as information only: as a WARN trigger it fired on 67.4% of starts.
1088
+ const startWave = recentStarts >= START_WAVE_MIN;
1089
+ const icon = sameCopy.length > 0 || startWave ? '⚠️ ' : '';
1011
1090
 
1012
1091
  // Semantic ids are NOT unique — several live sessions routinely share e.g.
1013
1092
  // `main-2026-08-21-session-2`, which makes a bare semantic label unusable
@@ -1019,6 +1098,9 @@ async function main() {
1019
1098
  return `${label}${disc}:${p.branch ?? 'unknown'}:wave-${p.current_wave ?? 0}`;
1020
1099
  };
1021
1100
  pushBanner(`${icon}👥 Peers: ${peers.length} live on this host`);
1101
+ if (startWave) {
1102
+ pushBanner(` ⚠️ Start wave: ${recentStarts} sessions started or resumed in the last ${START_WAVE_WINDOW_MIN} min — stagger further starts`);
1103
+ }
1022
1104
  if (sameCopy.length > 0) {
1023
1105
  const list = sameCopy.slice(0, 3).map(fmt).join(', ');
1024
1106
  const more = sameCopy.length > 3 ? ` +${sameCopy.length - 3} more` : '';
@@ -1174,7 +1256,7 @@ async function main() {
1174
1256
  pluginUpdate = await checkPluginUpdate({
1175
1257
  cacheDir: path.join(projectRoot, '.orchestrator', 'runtime'),
1176
1258
  // The probe's own fallback is env-only (SO_PLATFORM, then each harness's
1177
- // compat var) and cannot run detectPlatform()'s third step, the marker-
1259
+ // compat var) and cannot run detectPlatform()'s step 4, the marker-
1178
1260
  // directory walk from cwd. This hook already resolved the platform at the
1179
1261
  // top of run() through exactly that full precedence, so handing it over
1180
1262
  // makes the remedy instruction MEASURED rather than guessed — a Codex or
@@ -1195,8 +1277,16 @@ async function main() {
1195
1277
  // this root supersedes. `peer_count` is deliberately unchanged; this field
1196
1278
  // makes the supersession rate measurable instead of inferred (HR-105).
1197
1279
  peers_superseded: mechanicalPeersSuperseded,
1280
+ // #1650 — start-wave signal (HR-105): foreign registry entries whose
1281
+ // started_at lies within the last `recent_starts_window_min` minutes.
1282
+ recent_starts_count: recentStarts,
1283
+ recent_starts_window_min: START_WAVE_WINDOW_MIN,
1198
1284
  };
1199
1285
  if (headSha) payload.head_sha = headSha;
1286
+ if (upstreamSha !== null) {
1287
+ payload.upstream_sha = upstreamSha;
1288
+ payload.behind = behind;
1289
+ }
1200
1290
 
1201
1291
  // #1091 — record the native SessionStart source so "does the same raw
1202
1292
  // session_id repeat under source=resume?" becomes answerable from
package/hooks/on-stop.mjs CHANGED
@@ -47,6 +47,7 @@ import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
47
47
 
48
48
  import { emitEvent } from '../scripts/lib/events.mjs';
49
49
  import { detectPlatform, getProjectDir, resolveSessionRoot } from '../scripts/lib/platform.mjs';
50
+ import { resolveOwnLockRoot } from './_lib/hook-root.mjs';
50
51
  import { findOwnScopeFile } from '../scripts/lib/scope-gate.mjs';
51
52
  import {
52
53
  classifyManifestSession,
@@ -119,7 +120,7 @@ function discriminate(input) {
119
120
  // dependency degradation (GH Kanevry/session-orchestrator#63)
120
121
  // ---------------------------------------------------------------------------
121
122
  //
122
- // zx is loaded LAZILY. A static `import { $ } from 'zx'` fails at MODULE LOAD
123
+ // zx is loaded LAZILY. A static `import { $ } from '../scripts/lib/zx.mjs'` fails at MODULE LOAD
123
124
  // time when node_modules is absent (interrupted install, EPERM sandbox, half-
124
125
  // synced plugin cache), so the harness prints a 10-frame ERR_MODULE_NOT_FOUND
125
126
  // stack on EVERY turn end with no hint that `npm install` is the fix. This
@@ -175,7 +176,7 @@ function warnDependenciesMissingOnce() {
175
176
  */
176
177
  async function loadZx() {
177
178
  try {
178
- return (await import('zx')).$;
179
+ return (await import('../scripts/lib/zx.mjs')).$;
179
180
  } catch (err) {
180
181
  if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err;
181
182
  warnDependenciesMissingOnce();
@@ -401,11 +402,13 @@ function reaperTriggerField(trigger) {
401
402
  */
402
403
  async function handleStop(input, reaperTrigger = null) {
403
404
  // Two roots, by writer (#1511 point b). `current-session.json` and
404
- // `session.lock` (session id, lock heartbeat, duration) stay at the LAUNCH
405
- // dir: on-session-start.mjs writes both at `resolveProjectDir()`. The wave
405
+ // `session.lock` (session id, lock heartbeat, duration) are read where
406
+ // on-session-start.mjs wrote them: the launch dir, or — after EnterWorktree
407
+ // and a /clear, compact or resume there (#1605) — the entered worktree, found
408
+ // by the lock carrying this session's raw id (`resolveOwnLockRoot`). The wave
406
409
  // and the commit/branch describe the tree the session WORKS in — after
407
410
  // EnterWorktree that is the session root, not the launch checkout.
408
- const projectRoot = getProjectDir();
411
+ const projectRoot = resolveOwnLockRoot(input, getProjectDir());
409
412
  const sessionRoot = resolveSessionRoot(input?.cwd);
410
413
 
411
414
  const wave = await readWaveNumber(sessionRoot, input);
@@ -453,7 +456,9 @@ async function handleStop(input, reaperTrigger = null) {
453
456
  // is visible in sweep.log rather than invisible. One line per turn while
454
457
  // the entry is absent; that volume IS the signal, and the next
455
458
  // SessionStart's registerSelf() ends it.
456
- const refreshed = await heartbeat(sessionId);
459
+ // #1605 B4 — `work_root` follows the session into an entered worktree,
460
+ // so a relocating SessionStart of another session sees it working there.
461
+ const refreshed = await heartbeat(sessionId, { workRoot: path.resolve(sessionRoot) });
457
462
  if (refreshed === null) {
458
463
  logSweepEvent({
459
464
  event: 'heartbeat-missing',
@@ -100,7 +100,12 @@ async function resolveSessionId(input, projectDir) {
100
100
 
101
101
  let currentSession = null;
102
102
  if (projectDir) {
103
- const persisted = path.join(projectDir, '.orchestrator', 'current-session.json');
103
+ // #1605 — read THIS session's record where on-session-start.mjs wrote it
104
+ // (the entered worktree after a /clear there), found by the lock carrying
105
+ // the raw id. Lazy: only a matched issue command pays for the lookup.
106
+ const { resolveOwnLockRoot } = await import('./_lib/hook-root.mjs');
107
+ const ownRoot = resolveOwnLockRoot({ session_id: nativeRawId, cwd: input?.cwd }, projectDir);
108
+ const persisted = path.join(ownRoot, '.orchestrator', 'current-session.json');
104
109
  if (existsSync(persisted)) {
105
110
  try {
106
111
  currentSession = await readJson(persisted);
@@ -1093,9 +1093,10 @@ async function main() {
1093
1093
  // manifest a Bash call just corrupted. `'own'` and `'unknown'` (legacy,
1094
1094
  // unbound, or no resolvable own identity) fall through unchanged.
1095
1095
  //
1096
- // CEILING (BV-004): on a harness that supplies no session id at all (Codex
1097
- // CLI, Cursor today) the own-id set is empty, so this gate is permanently
1098
- // `'unknown'` = full pre-#1153 behaviour there.
1096
+ // CEILING (BV-004): on a harness that supplies no session id at all (Cursor
1097
+ // today) the own-id set is empty, so this gate is permanently `'unknown'` =
1098
+ // full pre-#1153 behaviour there. Codex CLI sends `session_id` in the hook
1099
+ // payload (measured 2026-10-20, codex-cli 0.160.1, #1691).
1099
1100
  {
1100
1101
  const { verdict, manifestIds } = classifyManifestSession(scope ?? {}, ownIds);
1101
1102
  if (verdict === 'foreign') {
@@ -1172,7 +1173,10 @@ async function main() {
1172
1173
  // `toContain('out-of-scope.mjs')` test to say otherwise.
1173
1174
  const missingSnapshotNotice = prevScopeState === null
1174
1175
  ? formatSnapshotMissingNotice({
1175
- sessionAge: sessionAgeMs(context.manifestRoot),
1176
+ // #1634: the clock lives beside this session's own lock, which after
1177
+ // ExitWorktree is its `session_root`, not the manifest root. Loaded lazily:
1178
+ // this branch is rare and the module graph costs ~7 ms per Bash call.
1179
+ sessionAge: sessionAgeMs((await import('./_lib/hook-root.mjs')).resolveOwnLockRoot(input, context.manifestRoot)),
1176
1180
  scopeAge: typeof scopeMtimeMs === 'number' ? Date.now() - scopeMtimeMs : null,
1177
1181
  reason: snapshot === null ? 'absent' : 'untrusted',
1178
1182
  })
@@ -14,22 +14,23 @@
14
14
  * G3 file_path present
15
15
  * G4 file extension filter — only .ts|.tsx|.js|.jsx|.mjs|.cjs
16
16
  * G5 gate check — wave-scope.json.gates['post-edit-validate'] === false → exit 0
17
- * G6 resolve typecheck command: Session Config → tsgo → tsc → npx tsc → skip
18
- * G7 run typecheck with 2s AbortController timeout
17
+ * G6 resolve typecheck command: Session Config → tsgo → tsc → local tsc → skip
18
+ * G7 run typecheck with 2s process-group timeout
19
19
  * G8 emit JSONL result to stderr
20
20
  *
21
- * Output (stderr): {"check":"typecheck","status":"pass|fail|skip","file":"<rel>","duration_ms":<int>}
21
+ * Output (stderr): {"check":"typecheck","status":"pass|fail|timeout|skip","file":"<rel>","duration_ms":<int>}
22
22
  * Exit codes: 0 — always (PostToolUse hooks are informational)
23
23
  *
24
24
  * SECURITY notes:
25
25
  * REQ-01 top-level try/catch → silent exit 0 on unexpected error (never blocks)
26
- * REQ-02 typecheck process uses AbortController (not shell `timeout` — cross-platform)
26
+ * REQ-02 typecheck runs the intact shell command; timeout kills its process tree
27
27
  * REQ-03 relative path computed via path.relative(projectRoot, absFilePath)
28
28
  */
29
29
 
30
30
  import path from 'node:path';
31
+ import { existsSync } from 'node:fs';
31
32
  import { spawnSync } from 'node:child_process';
32
- import { performance } from 'node:perf_hooks';
33
+ import { spawnInGroup } from '../scripts/lib/process-group.mjs';
33
34
 
34
35
  import { shouldRunHook } from './_lib/profile-gate.mjs';
35
36
  import { isMainModule } from '../scripts/lib/is-main-module.mjs';
@@ -54,9 +55,9 @@ const TS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
54
55
  /**
55
56
  * Emit the JSONL result line to stderr.
56
57
  * @param {string} file - relative file path
57
- * @param {'pass'|'fail'|'skip'} status
58
+ * @param {'pass'|'fail'|'timeout'|'skip'} status
58
59
  * @param {number} [duration_ms] - omit for skip
59
- * @param {string} [reason] - human-readable reason for skip or fail
60
+ * @param {string} [reason] - human-readable reason for skip, timeout or fail
60
61
  * @param {string} [remediation] - actionable fix hint for fail status only
61
62
  */
62
63
  function emitResult(file, status, duration_ms, reason, remediation) {
@@ -69,10 +70,10 @@ function emitResult(file, status, duration_ms, reason, remediation) {
69
70
 
70
71
  /**
71
72
  * Try to resolve the typecheck command.
72
- * Order: Session Config typecheck-command → tsgo → tsc → npx tsc → null
73
+ * Order: Session Config typecheck-command → tsgo → tsc → local tsc → null
73
74
  *
74
75
  * @param {string} projectRoot
75
- * @returns {Promise<{cmd: string, args: string[]}|null>}
76
+ * @returns {Promise<string|string[]|null>}
76
77
  */
77
78
  async function resolveTypecheckCommand(projectRoot) {
78
79
  // 1. Session Config typecheck-command
@@ -81,8 +82,8 @@ async function resolveTypecheckCommand(projectRoot) {
81
82
  const config = parseSessionConfig(md);
82
83
  const cmd = config['typecheck-command'];
83
84
  if (cmd && typeof cmd === 'string') {
84
- const parts = cmd.trim().split(/\s+/);
85
- return { cmd: parts[0], args: parts.slice(1) };
85
+ const command = cmd.trim();
86
+ return command === 'skip' ? null : command || null;
86
87
  }
87
88
  } catch {
88
89
  // config unavailable — fall through
@@ -90,15 +91,16 @@ async function resolveTypecheckCommand(projectRoot) {
90
91
 
91
92
  // 2. tsgo
92
93
  const tsgoParts = _tryWhich('tsgo');
93
- if (tsgoParts) return { cmd: tsgoParts, args: ['--noEmit'] };
94
+ if (tsgoParts) return 'tsgo --noEmit';
94
95
 
95
96
  // 3. tsc
96
97
  const tscParts = _tryWhich('tsc');
97
- if (tscParts) return { cmd: tscParts, args: ['--noEmit'] };
98
+ if (tscParts) return 'tsc --noEmit';
98
99
 
99
- // 4. npx tsc
100
- const npxParts = _tryWhich('npx');
101
- if (npxParts) return { cmd: npxParts, args: ['tsc', '--noEmit'] };
100
+ // 4. Only an installed project compiler; npx may install the unrelated
101
+ // npm package `tsc` in projects without TypeScript (#1612).
102
+ const localTsc = path.join(projectRoot, 'node_modules', 'typescript', 'bin', 'tsc');
103
+ if (existsSync(localTsc)) return [process.execPath, localTsc, '--noEmit'];
102
104
 
103
105
  return null;
104
106
  }
@@ -111,9 +113,11 @@ async function resolveTypecheckCommand(projectRoot) {
111
113
  * @returns {string|null}
112
114
  */
113
115
  function _tryWhich(name) {
114
- const whichCmd = process.platform === 'win32' ? 'where' : 'which';
116
+ const whichCmd = process.platform === 'win32' ? 'where.exe' : 'which';
117
+ // Two lookups get at most 1s total; revisit together with the outer
118
+ // 5s hook budget if a host needs slower executable discovery.
115
119
  try {
116
- const result = spawnSync(whichCmd, [name], { encoding: 'utf8', timeout: 2000 });
120
+ const result = spawnSync(whichCmd, [name], { encoding: 'utf8', timeout: 500 });
117
121
  if (result.status === 0 && result.stdout.trim()) return name;
118
122
  } catch {
119
123
  // ignore
@@ -122,43 +126,32 @@ function _tryWhich(name) {
122
126
  }
123
127
 
124
128
  /**
125
- * Run the typecheck command with a 2s AbortController timeout.
126
- *
127
- * @param {string} cmd
128
- * @param {string[]} args
129
- * @returns {{status: 'pass'|'fail', duration_ms: number, reason?: string}}
129
+ * Configured commands retain shell semantics; the local compiler uses Node argv.
130
+ * Keep cleanup short for hooks.json's 5s budget; Windows skips CIM verification.
131
+ * @param {string|string[]} cmd
132
+ * @param {string} projectRoot
133
+ * @returns {Promise<{status: 'pass'|'fail'|'timeout', duration_ms: number, reason?: string}>}
130
134
  */
131
- function runTypecheck(cmd, args) {
132
- const controller = new AbortController();
133
- const timer = setTimeout(() => controller.abort(), 2000);
134
-
135
- const start = performance.now();
136
- let spawnResult;
137
- try {
138
- spawnResult = spawnSync(cmd, args, {
139
- signal: controller.signal,
140
- encoding: 'utf8',
141
- stdio: ['ignore', 'pipe', 'pipe'],
142
- timeout: 2000,
143
- });
144
- } finally {
145
- clearTimeout(timer);
146
- }
147
-
148
- const duration_ms = Math.max(0, Math.round(performance.now() - start));
149
-
150
- // AbortError or SIGTERM from timeout
151
- if (spawnResult.error?.name === 'AbortError' || spawnResult.signal === 'SIGTERM') {
152
- return { status: 'fail', duration_ms, reason: 'typecheck timed out after 2s' };
135
+ async function runTypecheck(cmd, projectRoot) {
136
+ const result = await spawnInGroup(cmd, {
137
+ cwd: projectRoot,
138
+ timeoutMs: 2000,
139
+ // Hook-specific ceiling: escalate quickly so an uncooperative compiler
140
+ // cannot consume the outer 5s hook budget. Revisit if compiler cleanup
141
+ // needs more than 100ms; the hook's outer budget must change with it.
142
+ killGraceMs: 100,
143
+ verifyWaitMs: 100,
144
+ // Two PowerShell startups would exceed the remaining hook budget (R1-04).
145
+ verifyTree: false,
146
+ });
147
+ const duration_ms = Math.max(0, Math.round(result.durationMs));
148
+ if (result.timedOut) {
149
+ return { status: 'timeout', duration_ms, reason: 'typecheck timed out after 2s' };
153
150
  }
154
-
155
- if (spawnResult.status !== 0) {
156
- // Capture the first non-empty line of stderr/stdout as a compact reason.
157
- const combined = ((spawnResult.stderr ?? '') + (spawnResult.stdout ?? '')).trim();
158
- const firstLine = combined.split('\n').find((l) => l.trim()) ?? 'typecheck exited non-zero';
151
+ if (result.exitCode !== 0) {
152
+ const firstLine = result.fullOutput.split('\n').find((line) => line.trim()) ?? 'typecheck exited non-zero';
159
153
  return { status: 'fail', duration_ms, reason: firstLine.slice(0, 256) };
160
154
  }
161
-
162
155
  return { status: 'pass', duration_ms };
163
156
  }
164
157
 
@@ -216,7 +209,7 @@ async function main() {
216
209
  }
217
210
 
218
211
  // G7: run typecheck with 2s timeout (REQ-02)
219
- const { status, duration_ms, reason } = runTypecheck(tc.cmd, tc.args);
212
+ const { status, duration_ms, reason } = await runTypecheck(tc, projectRoot);
220
213
 
221
214
  // G8: emit JSONL result to stderr
222
215
  const remediation =