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
@@ -43,7 +43,7 @@
43
43
  * G1 tool filter — Bash only
44
44
  * G2 command must be a non-empty string
45
45
  * G3 cheap regex PRE-FILTER — skip the tokenizer for plainly unrelated
46
- * commands (see GIT_ADD_REGEX; it does not decide the question)
46
+ * commands (see mayStage; it does not decide the question)
47
47
  * G4 context gate — isWaveAgentContext(); exit 0 immediately for
48
48
  * coordinator/manual commits (AC5 safe-default no-op)
49
49
  * G4b tokenize + extract path operands; exit 0 when no `git add` statement
@@ -67,7 +67,8 @@
67
67
 
68
68
  import { readStdin, emitAllow, writeJsonAtomicSync } from '../scripts/lib/io.mjs';
69
69
  import { isWaveAgentContext } from '../scripts/lib/wave-context.mjs';
70
- import { existsSync, readFileSync } from 'node:fs';
70
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
71
+ import { execFileSync } from 'node:child_process';
71
72
  import path from 'node:path';
72
73
  import os from 'node:os';
73
74
  import crypto from 'node:crypto';
@@ -87,16 +88,38 @@ import { dotGitAncestor, launchDirFromEnv } from '../scripts/lib/platform.mjs';
87
88
  * command?" — since #1404 that verdict belongs to {@link extractStagedPaths},
88
89
  * which resolves the verb and subcommand properly.
89
90
  *
90
- * `git` and `add` must sit in the same statement (the negated class stops at
91
- * `;`, `&`, `|` and newline) within 120 characters of each other, so global
92
- * flags between them are tolerated: `git -C sub add x` reaches the extractor,
93
- * where `\bgit\s+add\b` used to drop it silently. `\badd\b` keeps
94
- * `git addremote` out.
91
+ * The words `git` and `add` must both occur — anywhere, in any order. Global
92
+ * flags between them are therefore tolerated at any length: `git -C sub add x`
93
+ * reaches the extractor, where `\bgit\s+add\b` used to drop it silently.
94
+ * `\badd\b` keeps `git addremote` out.
95
+ *
96
+ * Why not "same statement, within N characters" (#1670 item 6): the previous
97
+ * `\bgit\b[^;&|\n]{0,120}\badd\b` let `git -C <long path> -c k=v … add x`, a
98
+ * quoted `;`/`&`/`|` in a `-c` value, and a `\`-newline continuation pass
99
+ * UNFENCED — this pre-filter is the fence's only early exit, so every miss
100
+ * fails OPEN. Dropping the bound (`[^;&|\n]*`) is not an option either: it
101
+ * backtracks once per `git` occurrence, quadratic on attacker-influenced text
102
+ * (measured 2026-10-18: 200 kB of `git ` → 6220 ms, past the 5 s hook budget;
103
+ * two independent word tests → 0.16 ms). Two `\b` tests are linear.
95
104
  *
96
105
  * False positives are cheap AND inert: `git commit -m "add x"` matches here,
97
106
  * resolves to no `git add` statement, and writes nothing (G4b).
107
+ *
108
+ * `stage` is git's built-in synonym for `add` (#1671 item 5).
109
+ *
110
+ * NAMED CEILING (BV-004, #1671 item 5): a subcommand spelled with shell quoting
111
+ * or escaping — `git a\dd x`, `git a''dd x` — misses both this pre-filter and
112
+ * the extractor's unquoted-subcommand check, so that staging is not recorded.
113
+ * The fence only RECORDS (it never blocks), so the loss is a collision entry,
114
+ * not a bypassed denial. REVISIT TRIGGER: a wave agent observed staging with a
115
+ * quoted/escaped subcommand, or the fence starting to block.
116
+ *
117
+ * @param {string} command
118
+ * @returns {boolean}
98
119
  */
99
- const GIT_ADD_REGEX = /\bgit\b[^;&|\n]{0,120}\badd\b/;
120
+ function mayStage(command) {
121
+ return /\b(?:add|stage)\b/.test(command) && /\bgit\b/.test(command);
122
+ }
100
123
 
101
124
  /**
102
125
  * Recorded in place of a path list when the staging command names NO explicit
@@ -114,6 +137,9 @@ const GIT_GLOBAL_VALUE_FLAGS = new Set([
114
137
  '-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env',
115
138
  ]);
116
139
 
140
+ /** `git add` and its built-in synonym `git stage` (#1671 item 5). */
141
+ const GIT_ADD_SUBCOMMANDS = new Set(['add', 'stage']);
142
+
117
143
  /** `git add` flags that consume the NEXT token as their value. */
118
144
  const GIT_ADD_VALUE_FLAGS = new Set(['--chmod']);
119
145
 
@@ -211,7 +237,7 @@ function normalizeStagedPath(raw) {
211
237
  * blocked commit, which the hook's fail-safe posture forbids. This is also
212
238
  * why a blanket "pre-filter hit but nothing resolved → `paths: ['*']`"
213
239
  * floor was REJECTED at the 2026-09-20 fix: `git commit -m "add x"` matches
214
- * {@link GIT_ADD_REGEX}, and the marker overlaps EVERY staged path, so that
240
+ * {@link mayStage}, and the marker overlaps EVERY staged path, so that
215
241
  * floor manufactures a collision for a command that stages nothing.
216
242
  * - A staging command a shell never executes locally is not recorded, and
217
243
  * should not be: `ssh host 'git add x'` stages a REMOTE index, and
@@ -311,7 +337,7 @@ function scanForStagedPaths(command, lib, paths, state, depth) {
311
337
  // The G3 pre-filter again, on the payload: same cheap question, same
312
338
  // tolerated false positives, and it keeps an unrelated `-c` operand
313
339
  // from paying for a second tokenize.
314
- if (typeof payload === 'string' && GIT_ADD_REGEX.test(payload)) {
340
+ if (typeof payload === 'string' && mayStage(payload)) {
315
341
  scanForStagedPaths(payload, lib, paths, state, depth + 1);
316
342
  }
317
343
  }
@@ -335,7 +361,7 @@ function scanForStagedPaths(command, lib, paths, state, depth) {
335
361
  i += 1; // boolean global flag, or attached `--git-dir=<p>`
336
362
  }
337
363
 
338
- if (i >= segment.length || segment[i].quoted || segment[i].text !== 'add') continue;
364
+ if (i >= segment.length || segment[i].quoted || !GIT_ADD_SUBCOMMANDS.has(segment[i].text)) continue;
339
365
  state.recognised = true;
340
366
  i += 1;
341
367
 
@@ -485,6 +511,218 @@ function appendIntent({ fenceFile, agentId, command, paths }) {
485
511
  return writeJsonAtomicSync(fenceFile, body, { tmpPrefix: '.fence.tmp' });
486
512
  }
487
513
 
514
+ // ---------------------------------------------------------------------------
515
+ // Work-tree recorder (#1659)
516
+ // ---------------------------------------------------------------------------
517
+
518
+ /**
519
+ * Cheap PRE-FILTER for the work-tree recorder — same shape and same role as
520
+ * {@link mayStage}, for the same reasons (#1670 item 6: a bounded lookahead
521
+ * silently skipped `git -C <long path> -c … commit`): only a hit pays for the
522
+ * tokenizer and a git spawn. False positives (`echo "git commit"`) resolve to
523
+ * no commit statement and record nothing.
524
+ *
525
+ * @param {string} command
526
+ * @returns {boolean}
527
+ */
528
+ function mayCommit(command) {
529
+ return /\bcommit\b/.test(command) && /\bgit\b/.test(command);
530
+ }
531
+
532
+ /** Timeout for the one `git rev-parse` per commit tree (hook budget is 5 s). */
533
+ const RECORDER_GIT_TIMEOUT_MS = 2000;
534
+
535
+ /** Directory-changing verbs whose effect the walker does not model (#1659). */
536
+ const DIR_UNKNOWN_VERBS = new Set(['pushd', 'popd', 'chdir']);
537
+ /** Builtins that change the directory when invoked as `builtin <name>` (#1659). */
538
+ const DIR_CHANGE_BUILTINS = new Set(['cd', 'pushd', 'popd', 'chdir']);
539
+
540
+ /**
541
+ * Expand a `cd`/`-C` operand against the current directory. Returns null when
542
+ * the operand cannot be resolved without a shell (`$VAR`, `~user`, `-`), so
543
+ * the caller records nothing rather than a guessed tree.
544
+ *
545
+ * @param {string} base - absolute current directory
546
+ * @param {string} operand
547
+ * @returns {string|null}
548
+ */
549
+ function resolveDirOperand(base, operand) {
550
+ if (operand === '~') return os.homedir();
551
+ if (operand.startsWith('~/')) return path.join(os.homedir(), operand.slice(2));
552
+ if (operand === '-' || operand.startsWith('~') || operand.includes('$') || operand.includes('`')) return null;
553
+ return path.resolve(base, operand);
554
+ }
555
+
556
+ /**
557
+ * Effective directories of every `git commit` statement in a command.
558
+ *
559
+ * Walks the chain segments in order, tracking the directory the shell would be
560
+ * in: a `cd <dir>` statement moves it (relative to the previous one), and each
561
+ * `-C <dir>` global flag of the commit statement composes on top, as git does.
562
+ * A `cd` whose operand is unresolvable (`cd "$WT"`) makes the directory
563
+ * UNKNOWN for the rest of the command — later commits are not recorded rather
564
+ * than misattributed to the launch tree. So does any directory change the
565
+ * walker does not model (`pushd`, `popd`, `chdir`, `builtin cd`).
566
+ *
567
+ * NAMED CEILINGS (BV-004): subshell scope is not modelled — `(cd x && git
568
+ * commit); git commit` attributes the second commit to `x` too; a commit nested
569
+ * in `bash -c "…"` is not recorded. REVISIT if a session's commits are found
570
+ * missing from `orchestrator.session.work_tree` for one of these shapes.
571
+ *
572
+ * @param {string} command
573
+ * @param {string} startDir - absolute directory the command starts in
574
+ * @param {{ tokenizeCommand: Function, splitChainSegments: Function,
575
+ * resolveSegmentVerb: Function }} lib
576
+ * @returns {string[]} distinct absolute directories
577
+ */
578
+ function commitDirs(command, startDir, lib) {
579
+ const out = new Set();
580
+ let cwd = startDir;
581
+ const tokens = lib.tokenizeCommand(command);
582
+ // No paren stripping here: since #1667 the shared tokenizer turns subshell
583
+ // parens into statement breaks and appends `$( )`/backtick bodies as their
584
+ // own statements. A hand strip on top only ever changed `((git commit))`
585
+ // — arithmetic, not a commit — into a recorded commit (#1670).
586
+ //
587
+ // Those hoisted bodies sit AFTER every outer statement, behind the first
588
+ // `operator: 'substitution'` separator, so walking them in stream order would
589
+ // judge `cd sub && echo $(git commit); cd ../other` against `other`. Walk the
590
+ // outer statements first; the bodies then run in the start directory only
591
+ // when no outer statement moved it, and in an UNKNOWN directory otherwise
592
+ // (not recorded rather than misattributed).
593
+ const cut = tokens.findIndex((t) => t.operator === 'substitution');
594
+ const outer = cut < 0 ? tokens : tokens.slice(0, cut);
595
+ const hoistedSegments = cut < 0 ? [] : lib.splitChainSegments(tokens.slice(cut));
596
+ const outerSegments = lib.splitChainSegments(outer);
597
+ let moved = false;
598
+ const segments = [...outerSegments, null, ...hoistedSegments];
599
+ for (const segment of segments) {
600
+ if (segment === null) {
601
+ // Boundary between the outer statements and the hoisted bodies.
602
+ cwd = moved ? null : startDir;
603
+ continue;
604
+ }
605
+ if (segment.length === 0) continue;
606
+ const { verb, index } = lib.resolveSegmentVerb(segment);
607
+ if (index < 0) continue;
608
+
609
+ // A directory change the walker does not model (`pushd`, `popd`, zsh's
610
+ // `chdir`, `builtin cd …`) makes the directory UNKNOWN, like an
611
+ // unresolvable `cd` operand — never misattributed to the old tree (#1659).
612
+ if (!segment[index].quoted && (DIR_UNKNOWN_VERBS.has(verb)
613
+ || (verb === 'builtin' && index + 1 < segment.length && DIR_CHANGE_BUILTINS.has(segment[index + 1].text)))) {
614
+ cwd = null;
615
+ moved = true;
616
+ continue;
617
+ }
618
+
619
+ if (verb === 'cd' && !segment[index].quoted) {
620
+ moved = true;
621
+ if (cwd === null) continue;
622
+ let operand = null;
623
+ for (let j = index + 1; j < segment.length; j++) {
624
+ const t = segment[j];
625
+ if (!t.quoted && t.text.startsWith('-') && t.text !== '-') continue; // -P / -L
626
+ operand = t.text;
627
+ break;
628
+ }
629
+ cwd = operand === null ? os.homedir() : resolveDirOperand(cwd, operand);
630
+ continue;
631
+ }
632
+
633
+ if (verb !== 'git' || cwd === null) continue;
634
+ let dir = cwd;
635
+ let i = index + 1;
636
+ while (i < segment.length) {
637
+ const tok = segment[i];
638
+ if (tok.quoted || !tok.text.startsWith('-') || tok.text === '-') break;
639
+ if (GIT_GLOBAL_VALUE_FLAGS.has(tok.text)) {
640
+ if (tok.text === '-C' && i + 1 < segment.length && dir !== null) {
641
+ dir = resolveDirOperand(dir, segment[i + 1].text);
642
+ }
643
+ i += 2;
644
+ continue;
645
+ }
646
+ i += 1;
647
+ }
648
+ if (i < segment.length && !segment[i].quoted && segment[i].text === 'commit' && dir !== null) {
649
+ out.add(dir);
650
+ }
651
+ }
652
+ return [...out];
653
+ }
654
+
655
+ /**
656
+ * Record `(tree, start_ref)` for every tree a `git commit` in this command
657
+ * targets, as an `orchestrator.session.work_tree` event (#1659).
658
+ *
659
+ * Runs for EVERY session context (coordinator and wave agents alike), before
660
+ * the fence gates. Best-effort and side-effect-only: it never throws, never
661
+ * blocks, and never touches the hook's decision — the caller emits the same
662
+ * allow either way. One record per commit; the READER dedupes (first record
663
+ * per tree wins), so no ledger read happens on this path.
664
+ *
665
+ * Cost on a command without a commit: one or two linear word tests.
666
+ *
667
+ * @param {object} input - the hook payload
668
+ * @param {string} command
669
+ * @returns {Promise<void>}
670
+ */
671
+ async function recordWorkTrees(input, command) {
672
+ try {
673
+ if (!mayCommit(command)) return;
674
+ const sessionId = typeof input?.session_id === 'string' ? input.session_id.trim() : '';
675
+ if (sessionId === '') return;
676
+ const payloadCwd = typeof input?.cwd === 'string' ? input.cwd.trim() : '';
677
+ const startDir = path.isAbsolute(payloadCwd) ? payloadCwd : process.cwd();
678
+
679
+ const lib = await import('../scripts/lib/command-blocker.mjs');
680
+ const dirs = commitDirs(command, startDir, lib);
681
+ if (dirs.length === 0) return;
682
+
683
+ const pairs = new Map();
684
+ for (const dir of dirs) {
685
+ let lines;
686
+ try {
687
+ lines = execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel', 'HEAD'], {
688
+ encoding: 'utf8',
689
+ timeout: RECORDER_GIT_TIMEOUT_MS,
690
+ stdio: ['ignore', 'pipe', 'ignore'],
691
+ }).split('\n');
692
+ } catch {
693
+ continue; // not a repo, dir gone, or unborn HEAD — nothing to anchor on
694
+ }
695
+ const top = (lines[0] || '').trim();
696
+ const head = (lines[1] || '').trim();
697
+ if (top === '' || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head)) continue;
698
+ let tree = top;
699
+ try { tree = realpathSync(top); } catch { /* keep git's spelling */ }
700
+ if (!pairs.has(tree)) pairs.set(tree, head);
701
+ }
702
+ if (pairs.size === 0) return;
703
+
704
+ const { emitEvent } = await import('../scripts/lib/events.mjs');
705
+ const { SESSION_WORK_TREE_EVENT } = await import('../scripts/lib/events-schema.mjs');
706
+ for (const [tree, startRef] of pairs) {
707
+ try {
708
+ await emitEvent(SESSION_WORK_TREE_EVENT, {
709
+ session_id: sessionId,
710
+ tree,
711
+ start_ref: startRef,
712
+ });
713
+ } catch (err) {
714
+ process.stderr.write(
715
+ `⚠ pre-bash-staging-fence: work-tree record not written — ${err?.message || err}\n`,
716
+ );
717
+ }
718
+ }
719
+ } catch (err) {
720
+ process.stderr.write(
721
+ `⚠ pre-bash-staging-fence: work-tree recorder failed — ${err?.message || err}\n`,
722
+ );
723
+ }
724
+ }
725
+
488
726
  // ---------------------------------------------------------------------------
489
727
  // Main
490
728
  // ---------------------------------------------------------------------------
@@ -500,9 +738,13 @@ async function main() {
500
738
  const command = input?.tool_input?.command;
501
739
  if (typeof command !== 'string' || command.length === 0) return emitAllow();
502
740
 
741
+ // #1659 — record (tree, start_ref) before any `git commit`, in every session
742
+ // context. Side effect only: it cannot change the decision below.
743
+ await recordWorkTrees(input, command);
744
+
503
745
  // G3 — cheap regex PRE-FILTER. Commands that cannot possibly be a staging
504
746
  // command pass through without loading the tokenizer.
505
- if (!GIT_ADD_REGEX.test(command)) return emitAllow();
747
+ if (!mayStage(command)) return emitAllow();
506
748
 
507
749
  // G4 — context gate. Coordinator/manual commits are not fenced.
508
750
  if (!isWaveAgentContext()) return emitAllow();
@@ -541,8 +783,10 @@ if (isMainModule(import.meta.url)) {
541
783
  if (!shouldRunHook('pre-bash-staging-fence')) process.exit(0);
542
784
 
543
785
  // Top-level error handler — never let exit 1 leak. Fail-open on internal
544
- // errors to avoid blocking legitimate work (mirrors the destructive-guard
545
- // posture).
786
+ // errors on purpose (#1679 point 3): this hook only LOGS staging intent and
787
+ // never blocks (G6); the commit guard reconciles. Unlike the destructive
788
+ // guard, which denies on an internal error since #1671, a deny here would turn
789
+ // the logger into a gate. Revisit if a fence record is missed because of one.
546
790
  main().catch((e) => {
547
791
  process.stderr.write(
548
792
  `⚠ pre-bash-staging-fence: internal error — ${e?.message || e}\n`,
@@ -38,8 +38,9 @@
38
38
  */
39
39
 
40
40
  import { readStdin, emitAllow, emitDeny } from '../scripts/lib/io.mjs';
41
- import { resolveProjectDir, resolvePluginRoot } from '../scripts/lib/platform.mjs';
41
+ import { resolveProjectDir, resolvePluginRoot, resolveEventsLedgerRoot } from '../scripts/lib/platform.mjs';
42
42
  import { readJson } from '../scripts/lib/common.mjs';
43
+ import { readProcessLocalSessionIds } from '../scripts/lib/session-identity/own-session.mjs';
43
44
  import { hasReadInSession } from './_lib/transcript-history.mjs';
44
45
  import { resolveHost, matchesBypass } from './_lib/vcs-create-matcher.mjs';
45
46
 
@@ -219,7 +220,8 @@ function resolvePolicyPath(projectDir) {
219
220
  *
220
221
  * { "<session_id>": { "acknowledgedAt": "<ISO timestamp>" } }
221
222
  *
222
- * A future /templates-ack command writes this file; the hook only reads it.
223
+ * The `/templates-ack` command (commands/templates-ack.md, #1589/#1591) writes
224
+ * this file; the hook only reads it.
223
225
  *
224
226
  * @param {string} ackFilePath
225
227
  * @param {string|null} sessionId
@@ -239,29 +241,25 @@ async function isAcknowledged(ackFilePath, sessionId) {
239
241
  }
240
242
 
241
243
  /**
242
- * Pull the session_id from the hook stdin payload, with a single fallback
243
- * to the persisted file written by on-session-start.mjs. Returns null when
244
- * neither source yields a string.
244
+ * Require this invocation's ID for acknowledgement identity. Missing, malformed
245
+ * or conflicting IDs cannot fall back to env. Parent IDs and shared checkout
246
+ * state never establish an acknowledgement identity.
245
247
  *
246
248
  * @param {object|null} input
247
- * @param {string|null} projectDir
248
- * @returns {Promise<string|null>}
249
+ * @returns {string|null}
249
250
  */
250
- async function resolveSessionId(input, projectDir) {
251
- const fromStdin = input?.session_id ?? input?.sessionId ?? null;
252
- if (typeof fromStdin === 'string' && fromStdin.length > 0) return fromStdin;
253
-
254
- if (!projectDir) return null;
255
- const persisted = path.join(projectDir, '.orchestrator', 'current-session.json');
256
- if (!existsSync(persisted)) return null;
257
- try {
258
- const data = await readJson(persisted);
259
- if (data && typeof data === 'object') {
260
- const sid = data.session_id ?? data.sessionId ?? null;
261
- if (typeof sid === 'string' && sid.length > 0) return sid;
262
- }
263
- } catch {
264
- // ignore — null below
251
+ function resolveSessionId(input) {
252
+ const keys = ['session_id', 'sessionId'].filter((key) =>
253
+ Object.hasOwn(input ?? {}, key),
254
+ );
255
+ if (keys.length > 0) {
256
+ const ids = keys.map((key) => input[key]);
257
+ if (ids.some((id) => typeof id !== 'string' || !id.trim())) return null;
258
+ const trimmed = ids.map((id) => id.trim());
259
+ if (new Set(trimmed).size !== 1) return null;
260
+ // Pass only the invocation ID: the helper's parent/env union is not an
261
+ // acknowledgement identity and must not grant a peer's bypass.
262
+ return readProcessLocalSessionIds({ env: {}, hookInput: { session_id: trimmed[0] } })[0] ?? null;
265
263
  }
266
264
  return null;
267
265
  }
@@ -330,12 +328,15 @@ async function main() {
330
328
  typeof policy.acknowledgement_file === 'string' && policy.acknowledgement_file.length > 0
331
329
  ? policy.acknowledgement_file
332
330
  : DEFAULT_ACK_PATH;
331
+ // A relative ack file is runtime state, not a tracked file: resolve it under
332
+ // the MAIN checkout for a linked worktree, the same root `/templates-ack`
333
+ // writes to — otherwise the two sides split across roots (sweep of #1729).
333
334
  const ackFile = path.isAbsolute(ackRel)
334
335
  ? ackRel
335
- : path.join(projectDir || process.cwd(), ackRel);
336
+ : path.join(resolveEventsLedgerRoot(projectDir || process.cwd()), ackRel);
336
337
 
337
338
  // G6 — acknowledgement-file check.
338
- const sessionId = await resolveSessionId(input, projectDir);
339
+ const sessionId = resolveSessionId(input);
339
340
  if (await isAcknowledged(ackFile, sessionId)) {
340
341
  return emitAllow();
341
342
  }
@@ -430,9 +431,9 @@ if (isMainModule(import.meta.url)) {
430
431
  // per session.
431
432
  if (!shouldRunHook('pre-bash-templates-first')) process.exit(0);
432
433
 
433
- // Top-level error handler — never let exit 1 leak. Same posture as
434
- // pre-bash-destructive-guard.mjs: fail-open on internal errors to avoid
435
- // blocking legitimate work.
434
+ // Top-level error handler — never let exit 1 leak. This hook is advisory, so
435
+ // it fails open on internal errors to avoid blocking legitimate work (unlike
436
+ // pre-bash-destructive-guard.mjs, which denies on a main() error, #1671).
436
437
  main().catch((e) => {
437
438
  process.stderr.write(
438
439
  `⚠ pre-bash-templates-first: internal error — ${e?.message || e}\n`,
package/hooks/run-node.sh CHANGED
@@ -66,7 +66,7 @@ if [ -n "$expired" ]; then
66
66
  echo "session-orchestrator: 'node' not found on the hook PATH — plugin hooks are skipped."
67
67
  echo " Fix: install Node.js 24+, expose it on the harness PATH, or set SO_NODE_BIN=/abs/path/to/node."
68
68
  echo " (Hook shells do not source ~/.zshrc — Homebrew/nvm/volta installs can be invisible here."
69
- echo " Rate-limited to once per 6h. See README.md § Troubleshooting.)"
69
+ echo " Rate-limited to once per 6h. See docs/USER-GUIDE.md § Node is missing from the hook PATH.)"
70
70
  } >&2
71
71
  fi
72
72
  exit 0
@@ -33,7 +33,8 @@ import { spawn } from 'node:child_process';
33
33
 
34
34
  import { shouldRunHook } from './_lib/profile-gate.mjs';
35
35
  import { appendSkillInvocation } from '../scripts/lib/skill-invocations-schema.mjs';
36
- import { getProjectDir } from '../scripts/lib/platform.mjs';
36
+ import { resolveEventsLedgerRoot } from '../scripts/lib/platform.mjs';
37
+ import { resolveLedgerPath } from '../scripts/lib/ledger-path.mjs';
37
38
  import { shouldDailyFlush } from '../scripts/lib/telemetry/sync.mjs';
38
39
  import { resolveConsent, readTelemetryState } from '../scripts/lib/telemetry/consent.mjs';
39
40
  import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
@@ -44,19 +45,25 @@ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
44
45
  // ---------------------------------------------------------------------------
45
46
 
46
47
  /**
47
- * Metrics directory / ledger path. FUNCTIONS, not module-level consts:
48
- * `getProjectDir()` walks the filesystem on first call, and this hook must not
49
- * pay that at import time (#1153 P5).
48
+ * Ledger paths. FUNCTIONS, not module-level consts: resolving the project dir
49
+ * walks the filesystem on first call, and this hook must not pay that at
50
+ * import time (#1153 P5).
51
+ *
52
+ * The two resolve independently (#1722): `skill-invocations.jsonl` goes
53
+ * through {@link resolveLedgerPath} (the main checkout's copy only when the
54
+ * launch dir is a linked worktree that gitignores it), while the metrics dir
55
+ * holding `sessions.jsonl` maps unconditionally via resolveEventsLedgerRoot
56
+ * (#1514/#1584).
50
57
  *
51
58
  * @returns {string}
52
59
  */
53
- function metricsDirPath() {
54
- return path.join(getProjectDir(), '.orchestrator', 'metrics');
60
+ function sessionsMetricsDirPath() {
61
+ return path.join(resolveEventsLedgerRoot(), '.orchestrator', 'metrics');
55
62
  }
56
63
 
57
64
  /** @returns {string} */
58
65
  function jsonlPath() {
59
- return path.join(metricsDirPath(), 'skill-invocations.jsonl');
66
+ return resolveLedgerPath('.orchestrator/metrics/skill-invocations.jsonl');
60
67
  }
61
68
 
62
69
  /** Absolute path to the telemetry CLI (carries the hidden `_flush` subcommand). */
@@ -136,7 +143,7 @@ export function maybeSpawnDailyFlush({
136
143
  now = Date.now(),
137
144
  statePath,
138
145
  queuePath,
139
- metricsDir = metricsDirPath(),
146
+ metricsDir = sessionsMetricsDirPath(),
140
147
  } = {}) {
141
148
  try {
142
149
  // 1. Cheapest gate: env kill-switches, no file I/O.
@@ -108,12 +108,14 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
108
108
  import { isMainModule } from '../scripts/lib/is-main-module.mjs';
109
109
 
110
110
  import fs from 'node:fs';
111
- import path from 'node:path';
112
111
  import { appendSubagent } from '../scripts/lib/subagents-schema.mjs';
113
112
  import { getProjectDir } from '../scripts/lib/platform.mjs';
113
+ import { resolveLedgerPath } from '../scripts/lib/ledger-path.mjs';
114
114
  import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
115
115
  import { readTailWindow } from '../scripts/lib/tail-window.mjs';
116
116
  import { COST_BASIS } from '../scripts/lib/telemetry/pricing.mjs';
117
+ import { setLifecycle } from '../scripts/lib/agent-status.mjs';
118
+ import { rawPayloadSessionId, resolveOwnLockRoot } from './_lib/hook-root.mjs';
117
119
 
118
120
  // ---------------------------------------------------------------------------
119
121
  // Constants
@@ -127,7 +129,9 @@ import { COST_BASIS } from '../scripts/lib/telemetry/pricing.mjs';
127
129
  * @returns {string}
128
130
  */
129
131
  function jsonlPath() {
130
- return path.join(getProjectDir(), '.orchestrator', 'metrics', 'subagents.jsonl');
132
+ // #1722: the main checkout's copy when the launch dir is a linked worktree
133
+ // that gitignores the ledger (emit-session.mjs reads it there).
134
+ return resolveLedgerPath('.orchestrator/metrics/subagents.jsonl');
131
135
  }
132
136
 
133
137
  /**
@@ -650,7 +654,9 @@ function resolveSubagentTranscriptPath(parentTranscriptPath, agentId) {
650
654
 
651
655
  /**
652
656
  * Scan the tail of the ledger backwards for the most recent 'start' record with
653
- * the given agent_id and return its timestamp in epoch-ms (#917).
657
+ * the given agent_id and return its timestamp in epoch-ms (#917) plus its
658
+ * `agent_type` (#1617), so a stop whose payload carries no type can inherit it
659
+ * from the same single tail read — no second scan on the hot path.
654
660
  *
655
661
  * Reads only the last START_JOIN_TAIL_BYTES rather than the whole file — see that
656
662
  * constant for the measured sizing rationale. Scanning backwards means the FIRST
@@ -658,20 +664,22 @@ function resolveSubagentTranscriptPath(parentTranscriptPath, agentId) {
658
664
  * reused across sessions (measured 2026-07-30: 22 of 1420 distinct start ids
659
665
  * appeared more than once).
660
666
  *
661
- * NEVER throws. Any failure (missing file, unreadable, no match, unparseable
662
- * timestamp) yields null so the caller falls back to an honest "unknown".
667
+ * NEVER throws. Any failure (missing file, unreadable, no match) yields
668
+ * `{ startedAtMs: null, agentType: null }` so the caller falls back to an honest
669
+ * "unknown". An unparseable start timestamp nulls only `startedAtMs`.
663
670
  *
664
671
  * @param {string} filePath — ledger path
665
672
  * @param {string} agentId — the stopping agent's id
666
- * @returns {number|null} epoch-ms of the matching start, or null
673
+ * @returns {{ startedAtMs: number|null, agentType: string|null }}
667
674
  */
668
- function findStartTimestampMs(filePath, agentId) {
675
+ function findStartRecord(filePath, agentId) {
676
+ const none = { startedAtMs: null, agentType: null };
669
677
  try {
670
- if (typeof agentId !== 'string' || !agentId.trim()) return null;
671
- if (!fs.existsSync(filePath)) return null;
678
+ if (typeof agentId !== 'string' || !agentId.trim()) return none;
679
+ if (!fs.existsSync(filePath)) return none;
672
680
 
673
681
  const window = readTailWindow(filePath, START_JOIN_TAIL_BYTES);
674
- if (window.size === 0) return null;
682
+ if (window.size === 0) return none;
675
683
 
676
684
  const lines = window.text.split('\n');
677
685
  // When the window does not cover the whole file, the first element is a
@@ -692,11 +700,13 @@ function findStartTimestampMs(filePath, agentId) {
692
700
  }
693
701
  if (!obj || obj.event !== 'start' || obj.agent_id !== agentId) continue;
694
702
  const ms = Date.parse(obj.timestamp);
695
- return Number.isFinite(ms) ? ms : null;
703
+ const agentType =
704
+ typeof obj.agent_type === 'string' && obj.agent_type.trim() ? obj.agent_type : null;
705
+ return { startedAtMs: Number.isFinite(ms) ? ms : null, agentType };
696
706
  }
697
- return null;
707
+ return none;
698
708
  } catch {
699
- return null;
709
+ return none;
700
710
  }
701
711
  }
702
712
 
@@ -798,7 +808,23 @@ async function main() {
798
808
  // duration measurement and the explicit orphan discriminator below.
799
809
  // 'unknown' is the agent-id fallback for a payload with no usable id —
800
810
  // joining on it would collide across unrelated agents, so never scan for it.
801
- const startedAtMs = agentId === 'unknown' ? null : findStartTimestampMs(jsonlPath(), agentId);
811
+ const startJoin = agentId === 'unknown'
812
+ ? { startedAtMs: null, agentType: null }
813
+ : findStartRecord(jsonlPath(), agentId);
814
+ const { startedAtMs } = startJoin;
815
+
816
+ // agent_type inheritance (#1617): the stop payload does not always carry the
817
+ // type, while the start record for the same agent_id does. Payload wins when
818
+ // present; otherwise take it from the start record found by the join above.
819
+ // CEILING: this cannot type the phantom class (#939/#949) — those stops have
820
+ // no start record at all. Almost all type-less stop records are phantom
821
+ // stops (subagent_transcript_found: false), which this join cannot type;
822
+ // records from before #949 carry no flag at all. It closes the class for
823
+ // real agents whose stop payload omits the type.
824
+ // Readers counting by type must still filter on subagent_transcript_found.
825
+ if (agentType === null && startJoin.agentType !== null) {
826
+ record.agent_type = startJoin.agentType;
827
+ }
802
828
 
803
829
  // duration_ms (#917): prefer the harness value, else join backwards to this
804
830
  // agent's own start record, else null. Never 0 — see resolveDurationMs().
@@ -884,6 +910,43 @@ async function main() {
884
910
  }
885
911
 
886
912
  await appendSubagent(jsonlPath(), record);
913
+ await writeLiveMap(input, record);
914
+ }
915
+
916
+ /**
917
+ * Keep the HUD's agent live-map current from the lifecycle hooks (#1580).
918
+ * Before this, only wave-executor self-reports reached
919
+ * `.orchestrator/runtime/agent-status-current.json`, and those carry no session
920
+ * binding, so so-hud showed 0 agents while subagents ran.
921
+ *
922
+ * Writes into the root that holds THIS session's lock (#1605), so a session
923
+ * working in a worktree feeds the map the HUD reads beside that lock. A stop is
924
+ * written only when its start record was found: the phantom-stop class
925
+ * (#939/#949) has no start and a fresh agent_id per firing, and would otherwise
926
+ * add one `done` entry per firing. Best-effort: a short lock timeout and every
927
+ * failure swallowed, so agent dispatch is never delayed beyond it.
928
+ *
929
+ * @param {object} input Hook payload.
930
+ * @param {object} record The ledger record just written.
931
+ * @returns {Promise<void>}
932
+ */
933
+ async function writeLiveMap(input, record) {
934
+ try {
935
+ if (record.agent_id === 'unknown') return;
936
+ if (record.event === 'stop' && record.start_record_found !== true) return;
937
+ const sessionId = rawPayloadSessionId({ session_id: record.parent_session_id });
938
+ if (sessionId === null) return;
939
+ const repoRoot = resolveOwnLockRoot({ session_id: sessionId, cwd: input?.cwd }, getProjectDir());
940
+ await setLifecycle(
941
+ record.agent_id,
942
+ { status: record.event === 'start' ? 'running' : 'done', sessionId, agentType: record.agent_type ?? null },
943
+ // The event's own time, not the time after transcript parsing: a stop stamped late can overtake a
944
+ // quickly resumed agent's next start and show it as done.
945
+ { repoRoot, timeoutMs: 1000, now: Number.isFinite(Date.parse(record.timestamp)) ? Date.parse(record.timestamp) : Date.now() },
946
+ );
947
+ } catch {
948
+ /* best-effort: the live-map never blocks or fails the hook */
949
+ }
887
950
  }
888
951
 
889
952
  // Entry guard (#1393): run only as the node script the harness execs — a bare