session-orchestrator 5.2.0 → 5.4.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 (427) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -36,10 +36,9 @@ import { randomUUID } from 'node:crypto';
36
36
  import path from 'node:path';
37
37
 
38
38
  import { shouldRunHook } from './_lib/profile-gate.mjs';
39
- // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
40
- if (!shouldRunHook('on-session-start')) process.exit(0);
41
39
 
42
40
  import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
41
+ import { readNavigatorLease } from '../scripts/lib/fleet-protocol.mjs';
43
42
  import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
44
43
  import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
45
44
  import { getPlatform, resolveProjectDir } from '../scripts/lib/platform.mjs';
@@ -54,6 +53,7 @@ import { detectColdStart, consumeMarker } from '../scripts/lib/cold-start-detect
54
53
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
55
54
  import { readTelemetryState, resolveConsent, isCiEnv } from '../scripts/lib/telemetry/consent.mjs';
56
55
  import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
56
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
57
57
  // SSOT for the "is this a re-entry into the same logical session?" question
58
58
  // (#1091). Defined in the lock-bootstrap leaf module, which this hook already
59
59
  // loads, so the preservation branch below and the lock force-refresh gate can
@@ -99,6 +99,38 @@ const bannerLines = [];
99
99
  */
100
100
  let pendingAdditionalContext = null;
101
101
 
102
+ /** Byte budget of the navigator line (#1462, PRD 4.5 row "Bannerzeile B6"). */
103
+ const NAVIGATOR_BANNER_MAX_BYTES = 120;
104
+
105
+ /**
106
+ * Render the one-line navigator status (#1462, fleet protocol v1). The verdict
107
+ * comes from `readNavigatorLease()`: only a VALID lease is "aktiv"; a missing or
108
+ * expired one is "nicht nachweisbar" (ADR-004: absence is no proof of a free
109
+ * seat), an unreadable one says so instead of guessing. A long id is cut from
110
+ * the label — never from the fixed text — so the whole line stays ≤ 120 bytes.
111
+ * Same wording on every harness (Codex runs this hook too).
112
+ */
113
+ export function navigatorBannerLine(verdict) {
114
+ if (verdict?.state === 'active') {
115
+ const lease = verdict.lease ?? {};
116
+ const prefix = '🧭 Navigator aktiv: ';
117
+ const suffix = ', Check-in nach ~/.config/navigator/checkin/';
118
+ const room = NAVIGATOR_BANNER_MAX_BYTES - Buffer.byteLength(prefix + suffix, 'utf8');
119
+ // Defence in depth: the lease reader already refuses control bytes in these
120
+ // two fields; a newline / ANSI / bidi byte must still never reach systemMessage.
121
+ let shown = String(lease.adresse ?? lease.session_id ?? 'unknown')
122
+ .replace(/[\p{Cc}\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu, '?');
123
+ if (Buffer.byteLength(shown, 'utf8') > room) {
124
+ const chars = [...shown];
125
+ while (chars.length > 0 && Buffer.byteLength(`${chars.join('')}…`, 'utf8') > room) chars.pop();
126
+ shown = `${chars.join('')}…`;
127
+ }
128
+ return `${prefix}${shown}${suffix}`;
129
+ }
130
+ if (verdict?.state === 'unreadable') return '🧭 Navigator: Lease unlesbar, Status nicht nachweisbar';
131
+ return '🧭 Navigator: keiner nachweisbar (keine gültige Lease)';
132
+ }
133
+
102
134
  /**
103
135
  * Queue one or more banner lines for the single end-of-hook flush.
104
136
  * @param {string|null|undefined} line — multi-line strings are pushed verbatim
@@ -453,8 +485,10 @@ async function resolveSessionId(input, projectRoot) {
453
485
  // observe. A naive full overwrite of current-session.json drops the
454
486
  // `last_wave` / `last_batch` markers written mid-session by
455
487
  // post-tool-batch-wave-signal.mjs, which makes the next PostToolBatch
456
- // re-read last_wave as absent→0 and re-emit a duplicate
457
- // orchestrator.wave.started{N} with no intervening wave.completed.
488
+ // re-read last_wave as absent→0 and RE-OPEN wave N mid-wave, re-stamping
489
+ // wave_start_sha and so moving the #980 files_changed start point. (Until
490
+ // 2026-09-19 it also re-emitted a duplicate wave.started{N}; that event
491
+ // has since been removed.)
458
492
  //
459
493
  // Three cases, exactly one of which preserves nothing:
460
494
  // 'raw-id' — same-logical-session source AND the recorded raw
@@ -761,11 +795,22 @@ async function main() {
761
795
  // Resolve current branch; fall back to "unknown" when detached HEAD or no git.
762
796
  const branch = (await gitOutput(['branch', '--show-current'], projectRoot)) ?? 'unknown';
763
797
 
798
+ // #1443 — the commit this session started on, the fallback source for
799
+ // `session_start_ref` (STATE.md's `session-start-ref` had no writer). Kept
800
+ // only when it is a full object name; no repo, an unborn HEAD or a missing
801
+ // git leaves it null and the payload key is OMITTED, never fabricated.
802
+ const rawHeadSha = await gitOutput(['rev-parse', 'HEAD'], projectRoot);
803
+ const headSha = rawHeadSha && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(rawHeadSha) ? rawHeadSha : null;
804
+
764
805
  // v3.1.0 env-aware banner (opt-out via enable-host-banner: false in Session Config).
765
806
  // The ask-via-tool nudge rides the same opt-out flag — both are coordinator
766
807
  // reminders shown at session start; users who silence one expect silence.
808
+ // Hoisted because the cold-start DISPLAY below rides the same opt-out (#1133)
809
+ // and `bannerData` is not a usable proxy for it: `emitHostBanner()` returns
810
+ // null on its own error paths, which would silently re-open the nudge.
811
+ const hostBannerEnabled = await isHostBannerEnabled(projectRoot);
767
812
  let bannerData = null;
768
- if (await isHostBannerEnabled(projectRoot)) {
813
+ if (hostBannerEnabled) {
769
814
  bannerData = await emitHostBanner(projectRoot);
770
815
  // Always-on nudge: a user decision has three legitimate forms and AUQ-001
771
816
  // routes between them in order — operator verb first (nothing is blocked
@@ -795,7 +840,14 @@ async function main() {
795
840
  enabled: coldStartCfg.enabled !== false,
796
841
  });
797
842
  if (decision.shouldEmit) {
798
- pushBanner(decision.bannerLines.join('\n'));
843
+ // #1133 — the DISPLAY rides the enable-host-banner opt-out like every
844
+ // other start-of-session emitter ("users who silence one expect
845
+ // silence"). The one-shot marker consumption below does NOT: the
846
+ // opt-out silences the display, never the measurement or the state
847
+ // transition. Consuming it unconditionally is what keeps the nudge
848
+ // one-shot for an operator who has banners off — otherwise it re-arms
849
+ // on every start forever.
850
+ if (hostBannerEnabled) pushBanner(decision.bannerLines.join('\n'));
799
851
  if (decision.markerPath) {
800
852
  await consumeMarker(decision.markerPath).catch(() => false);
801
853
  }
@@ -975,6 +1027,18 @@ async function main() {
975
1027
  pushBanner(' (registry view: repo/branch/wave. Liveness (busy/idle) is model-side — not available in this hook.)');
976
1028
  }
977
1029
 
1030
+ // #1462 — Navigator line (fleet protocol v1). Exactly one line, read from the
1031
+ // lease file only (`~/.config/navigator/leases/navigator.json`, override
1032
+ // NAVIGATOR_CONFIG_DIR); registry `role` and ListAgents are hints, never proof.
1033
+ // Gated like the peer lines above: no host banner → no navigator line. A read
1034
+ // failure must never block the hook — the helper is fail-closed itself, the
1035
+ // try/catch only guards against the unexpected.
1036
+ if (bannerData) {
1037
+ try {
1038
+ pushBanner(navigatorBannerLine(await readNavigatorLease()));
1039
+ } catch { /* swallow — hook must remain non-blocking */ }
1040
+ }
1041
+
978
1042
  // Epic #583 W3-P3 — Mechanical peer-detection banner (independent of the
979
1043
  // v3.1.0 registry-driven detectPeers() output). discoverActiveSessions()
980
1044
  // reads every worktree's session.lock and the host registry; this is the
@@ -1114,6 +1178,7 @@ async function main() {
1114
1178
  // makes the supersession rate measurable instead of inferred (HR-105).
1115
1179
  peers_superseded: mechanicalPeersSuperseded,
1116
1180
  };
1181
+ if (headSha) payload.head_sha = headSha;
1117
1182
 
1118
1183
  // #1091 — record the native SessionStart source so "does the same raw
1119
1184
  // session_id repeat under source=resume?" becomes answerable from
@@ -1219,6 +1284,14 @@ async function main() {
1219
1284
  // Size-based rotation of events.jsonl (#251). Session-start is the single
1220
1285
  // rotation trigger — per-append overhead is rejected design. Any failure
1221
1286
  // is swallowed: rotation must NEVER block the hook.
1287
+ //
1288
+ // #1401 — the DURABLE record of a rotation is no longer this block. Until
1289
+ // 2026-09-19 the only evidence a rotation had happened was the `console.error`
1290
+ // below, and this hook's stderr is discarded by the harness: a rotation and a
1291
+ // DELETED archive were byte-identical from outside. `maybeRotate()` now writes
1292
+ // an `orchestrator.events.rotated` record as the first line of the new active
1293
+ // file, so the ledger carries its own break whatever happens to this stderr
1294
+ // line. The line stays as an operator convenience only — never as the record.
1222
1295
  try {
1223
1296
  let rotCfg = { enabled: true, 'max-size-mb': 10, 'max-backups': 5 };
1224
1297
  try {
@@ -1234,7 +1307,13 @@ async function main() {
1234
1307
  enabled: rotCfg.enabled !== false,
1235
1308
  });
1236
1309
  if (result.rotated) {
1237
- console.error(`events-rotation: archived ${result.archivedAs} (${result.sizeBefore} bytes)`);
1310
+ const pruned = result.pruned?.length ? `, pruned ${result.pruned.length}` : '';
1311
+ const ledger = result.recordWritten ? '' : ' [WARN: rotation record NOT written]';
1312
+ console.error(
1313
+ `events-rotation: archived ${result.archivedAs} ` +
1314
+ `(${result.sizeBefore} bytes, ${result.lines ?? '?'} lines, ` +
1315
+ `${result.firstTs ?? 'unknown'} → ${result.lastTs ?? 'unknown'}${pruned})${ledger}`,
1316
+ );
1238
1317
  }
1239
1318
  } catch (err) {
1240
1319
  console.error(`events-rotation: skipped (${err?.message ?? err})`);
@@ -1245,7 +1324,18 @@ async function main() {
1245
1324
  // flushBanner() runs here too so a throw partway through main() still surfaces
1246
1325
  // whatever was already collected; it is idempotent, so the normal path (which
1247
1326
  // flushes at the end of main) does not double-emit.
1248
- main().catch(() => {}).finally(() => {
1249
- flushBanner();
1250
- process.exit(0);
1251
- });
1327
+ //
1328
+ // Entry guard (#1298 P7): every harness execs this file as the node script
1329
+ // (`sh run-node.sh <this file>` → `exec node "$@"`), so argv[1] IS this module
1330
+ // on every real path. A bare `import()` — a probe, a test, a curious agent —
1331
+ // must not run main() against the live repo: incident W4-FX1 overwrote
1332
+ // `.orchestrator/current-session.json` exactly that way.
1333
+ if (isMainModule(import.meta.url)) {
1334
+ // #1393: the profile gate sits INSIDE the entry guard — at module top level
1335
+ // its `process.exit(0)` exited every process that merely imported this hook.
1336
+ if (!shouldRunHook('on-session-start')) process.exit(0);
1337
+ main().catch(() => {}).finally(() => {
1338
+ flushBanner();
1339
+ process.exit(0);
1340
+ });
1341
+ }
package/hooks/on-stop.mjs CHANGED
@@ -30,28 +30,28 @@
30
30
  */
31
31
 
32
32
  import path from 'node:path';
33
+ import { spawn } from 'node:child_process';
34
+ import { performance } from 'node:perf_hooks';
33
35
  import {
34
36
  promises as fs,
35
- closeSync,
36
37
  existsSync,
37
- fstatSync,
38
- openSync,
39
38
  readFileSync,
40
- readSync,
41
39
  statSync,
42
40
  writeFileSync,
43
41
  } from 'node:fs';
42
+ import { fileURLToPath } from 'node:url';
44
43
 
45
44
  import { shouldRunHook } from './_lib/profile-gate.mjs';
45
+ import { readTailWindow } from '../scripts/lib/tail-window.mjs';
46
46
  import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
47
- // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
48
- if (!shouldRunHook('on-stop')) process.exit(0);
49
47
 
50
48
  import { emitEvent } from '../scripts/lib/events.mjs';
51
49
  import { detectPlatform, getProjectDir } from '../scripts/lib/platform.mjs';
52
50
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
51
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
53
52
  import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
54
53
  import { readLock, updateHeartbeat } from '../scripts/lib/session-lock.mjs';
54
+ import { _parseReaper } from '../scripts/lib/config/reaper.mjs';
55
55
 
56
56
  // ---------------------------------------------------------------------------
57
57
  // stdin reading (inline — no io.mjs because Stop hooks exit 0 always, never deny)
@@ -743,17 +743,11 @@ function firstNonEmptyString(input, keys) {
743
743
  * @returns {string|null}
744
744
  */
745
745
  function readStatusFromTranscriptTail(transcriptPath) {
746
- let fd = null;
747
746
  try {
748
- fd = openSync(transcriptPath, 'r');
749
- const size = fstatSync(fd).size;
750
- const length = Math.min(size, STATUS_TAIL_BYTES);
751
- const start = size - length;
752
- const buf = Buffer.alloc(length);
753
- const read = readSync(fd, buf, 0, length, start);
754
- let lines = buf.subarray(0, read).toString('utf8').split('\n');
747
+ const window = readTailWindow(transcriptPath, STATUS_TAIL_BYTES);
748
+ let lines = window.text.split('\n');
755
749
  // Drop the leading partial line when the window did not start at byte 0.
756
- if (start > 0) lines = lines.slice(1);
750
+ if (window.cut) lines = lines.slice(1);
757
751
 
758
752
  for (let i = lines.length - 1; i >= 0; i -= 1) {
759
753
  const line = lines[i].trim();
@@ -774,10 +768,6 @@ function readStatusFromTranscriptTail(transcriptPath) {
774
768
  return null;
775
769
  } catch {
776
770
  return null;
777
- } finally {
778
- if (fd !== null) {
779
- try { closeSync(fd); } catch { /* best-effort */ }
780
- }
781
771
  }
782
772
  }
783
773
 
@@ -785,10 +775,160 @@ function readStatusFromTranscriptTail(transcriptPath) {
785
775
  // main
786
776
  // ---------------------------------------------------------------------------
787
777
 
778
+ // ---------------------------------------------------------------------------
779
+ // orphan-reaper trigger (#1432 B4)
780
+ // ---------------------------------------------------------------------------
781
+ //
782
+ // DUPLICATED VERBATIM between the two trigger hooks —
783
+ // hooks/post-tool-batch-wave-signal.mjs <-> hooks/on-stop.mjs. Deliberate, with
784
+ // a named revisit trigger (BV-004): they are the only two trigger points the
785
+ // PRD declares, and a shared `hooks/_lib/reaper-trigger.mjs` would be a third
786
+ // file in the hook import graph. SIZE, measured 2026-09-22 by diffing the two
787
+ // blocks: 138 identical lines — this note said "~40 lines of glue" until then,
788
+ // which is what made the duplicate look cheaper than it is. Extract it the
789
+ // moment a THIRD hook needs the trigger, or the moment the copies must differ.
790
+
791
+ /**
792
+ * Filesystem path of the scan CLI, spawned as a PLAIN argv call.
793
+ *
794
+ * It used to be a `file://` URL handed to `node --input-type=module -e
795
+ * <program>`, because `scripts/lib/orphan-reaper.mjs` had no entry guard. It
796
+ * has one now (`parseReaperCliArgs` + the `isMainModule` tail), so the child's
797
+ * contract lives in that module instead of as source text duplicated here —
798
+ * and nothing this hook builds is a program any more: every variable part is
799
+ * an argv value, which cannot become code whatever the checkout path contains.
800
+ *
801
+ * `fileURLToPath`, not `new URL(...).pathname`: the latter leaves a
802
+ * percent-encoded path for any checkout directory containing a space.
803
+ */
804
+ const ORPHAN_REAPER_SCRIPT = fileURLToPath(
805
+ new URL('../scripts/lib/orphan-reaper.mjs', import.meta.url),
806
+ );
807
+
808
+ /**
809
+ * Read the `reaper:` block from the project's Session Config host file.
810
+ * Sync + inline, mirroring `hooks/loop-guard.mjs` `loadConfig()` — a hot hook
811
+ * path must not import the full config orchestrator. A missing or unreadable
812
+ * file yields the parser defaults, i.e. DISABLED.
813
+ *
814
+ * @param {string} projectDir
815
+ * @returns {ReturnType<typeof _parseReaper>}
816
+ */
817
+ function loadReaperConfig(projectDir) {
818
+ for (const name of ['CLAUDE.md', 'AGENTS.md']) {
819
+ try {
820
+ return _parseReaper(readFileSync(path.join(projectDir, name), 'utf8'));
821
+ } catch {
822
+ // missing or unreadable — try the next candidate
823
+ }
824
+ }
825
+ return _parseReaper('');
826
+ }
827
+
828
+ /**
829
+ * Trigger the orphan scan, throttled and NON-BLOCKING (PRD FA4).
830
+ *
831
+ * The hook itself does exactly two pieces of I/O — one config read and one
832
+ * `stat` of the throttle marker — and then hands the work to a DETACHED,
833
+ * unref'd child. The scan is never run inline: one `ps` round-trip out of Node
834
+ * was measured at ~47 ms over 287 KB of output, which alone would blow the
835
+ * 50 ms `reaper.max-hook-latency-ms` budget this hook has to stay inside.
836
+ *
837
+ * The marker is stamped BEFORE the spawn, so a spawn that fails still consumes
838
+ * the throttle window — otherwise a broken spawn would be retried on every
839
+ * single tool batch.
840
+ *
841
+ * `reaper.max-hook-latency-ms` is the ceiling on the work above — measured here
842
+ * with `performance.now()` and reported as ONE stderr WARN line when exceeded.
843
+ * A WARN and not an event: this fires from a `PostToolBatch`-class hook, and a
844
+ * per-fire telemetry record is exactly the always-on signal
845
+ * `.claude/rules/host-resources.md` HR-101 calls a broken instrument. The
846
+ * measurement covers preparation only — the scan itself runs detached, which is
847
+ * the whole reason the budget can be held.
848
+ *
849
+ * Never throws: any failure degrades silently (PRD FA4 "lautlos degradieren").
850
+ *
851
+ * @param {object} [opts]
852
+ * @param {string} [opts.projectDir] Repo root; defaults to `getProjectDir()`.
853
+ * @param {number} [opts.now] Injected clock (ms).
854
+ * @param {Function} [opts.spawnFn] Injected `spawn` (tests).
855
+ * @param {Function} [opts.statFn] Injected `statSync` (tests).
856
+ * @param {Function} [opts.writeFn] Injected marker writer (tests).
857
+ * @param {() => number} [opts.clockFn] Injected monotonic clock for the latency
858
+ * budget (tests); defaults to `performance.now`.
859
+ * @returns {Promise<{spawned: boolean, reason: string}>}
860
+ */
861
+ export async function maybeTriggerOrphanScan({
862
+ projectDir,
863
+ now,
864
+ spawnFn = spawn,
865
+ statFn,
866
+ writeFn,
867
+ clockFn = () => performance.now(),
868
+ } = {}) {
869
+ const startedAt = clockFn();
870
+ /** One WARN line when the preparation overran `reaper.max-hook-latency-ms`. */
871
+ const checkLatency = (budgetMs) => {
872
+ const elapsed = clockFn() - startedAt;
873
+ if (Number.isFinite(budgetMs) && budgetMs > 0 && elapsed > budgetMs) {
874
+ process.stderr.write(
875
+ `orphan-reaper trigger: hook latency ${elapsed.toFixed(1)} ms exceeded `
876
+ + `reaper.max-hook-latency-ms (${budgetMs} ms)\n`,
877
+ );
878
+ }
879
+ };
880
+ try {
881
+ const root = typeof projectDir === 'string' && projectDir ? projectDir : getProjectDir();
882
+ const cfg = loadReaperConfig(root);
883
+ // Cheapest gate first: disabled means no stat, no spawn, no module load.
884
+ if (cfg.enabled !== true) return { spawned: false, reason: 'disabled' };
885
+
886
+ // The throttle module only — the scan's ledger, `ps` and kill-ladder code
887
+ // load in the detached child, never on this hook path (#1437).
888
+ const reaper = await import('../scripts/lib/orphan-reaper/scan-throttle.mjs');
889
+ const markerPath = reaper.scanMarkerPath(root);
890
+ const nowMs = typeof now === 'number' ? now : Date.now();
891
+
892
+ if (!reaper.shouldScanNow(markerPath, nowMs, cfg['min-scan-interval-seconds'], { statFn })) {
893
+ checkLatency(cfg['max-hook-latency-ms']);
894
+ return { spawned: false, reason: 'throttled' };
895
+ }
896
+ reaper.touchScanMarker(markerPath, { writeFn });
897
+
898
+ const child = spawnFn(
899
+ process.execPath,
900
+ [
901
+ ORPHAN_REAPER_SCRIPT,
902
+ '--repo-root', root,
903
+ '--mode', cfg.mode,
904
+ '--min-age-seconds', String(cfg['min-age-seconds']),
905
+ '--kill-grace-ms', String(cfg['kill-grace-ms']),
906
+ '--verify-wait-ms', String(cfg['verify-wait-ms']),
907
+ '--false-alarm-window', String(cfg['false-alarm-window']),
908
+ ],
909
+ { detached: true, stdio: 'ignore' },
910
+ );
911
+ if (child && typeof child.unref === 'function') child.unref();
912
+ checkLatency(cfg['max-hook-latency-ms']);
913
+ return { spawned: true, reason: 'spawned' };
914
+ } catch {
915
+ return { spawned: false, reason: 'error' };
916
+ }
917
+ }
918
+
788
919
  async function main() {
789
920
  const input = await readStdinJson();
790
921
  const eventType = discriminate(input);
791
922
 
923
+ // Orphan-reaper trigger (#1432 B4) — SubagentStop is the PRD's second trigger
924
+ // point: it lands exactly when gate children have just finished and their PIDs
925
+ // are still known. Placed BEFORE the branch dispatch (unlike the PostToolBatch
926
+ // hook, which triggers last) because both branches below may throw — a
927
+ // `handleStop` failure must not silently disarm the watchdog. The cost is one
928
+ // config read plus one stat, bounded by `reaper.max-hook-latency-ms` (50 ms),
929
+ // and the scan itself runs in a detached child.
930
+ await maybeTriggerOrphanScan();
931
+
792
932
  if (eventType === 'subagent_stop') {
793
933
  const additionalContext = await handleSubagentStop(input);
794
934
  // v2.1.163+: emit hookSpecificOutput for SubagentStop path.
@@ -860,6 +1000,14 @@ function buildTerminalSequenceJson(platform) {
860
1000
  }
861
1001
 
862
1002
  // Exit 0 always — informational hook must never block Claude.
863
- main()
864
- .catch(() => {})
865
- .finally(() => process.exit(0));
1003
+ // Entry guard (#1298 P7): run only as the node script every harness execs
1004
+ // (`sh run-node.sh <this file>`); a bare `import()` must not run the Stop
1005
+ // handler against the live repo.
1006
+ if (isMainModule(import.meta.url)) {
1007
+ // #1393: the profile gate sits INSIDE the entry guard — at module top level
1008
+ // its `process.exit(0)` exited every process that merely imported this hook.
1009
+ if (!shouldRunHook('on-stop')) process.exit(0);
1010
+ main()
1011
+ .catch(() => {})
1012
+ .finally(() => process.exit(0));
1013
+ }
@@ -34,8 +34,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
34
34
  import path from 'node:path';
35
35
 
36
36
  import { shouldRunHook } from './_lib/profile-gate.mjs';
37
- // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
38
- if (!shouldRunHook('operator-steer')) process.exit(0);
37
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
39
38
 
40
39
  import { getProjectDir } from '../scripts/lib/platform.mjs';
41
40
 
@@ -61,4 +60,13 @@ async function main() {
61
60
  }
62
61
 
63
62
  // Exit 0 always — informational hook must never block Claude.
64
- main().catch(() => {}).finally(() => process.exit(0));
63
+ // Entry guard (#1393): run only when this file IS the script node was invoked
64
+ // with — every harness path execs it (`sh run-node.sh <this file>`). A bare
65
+ // `import()` (a probe, a test, a curious agent) must neither run main() nor
66
+ // tear the importing process down. The profile gate sits INSIDE the guard for
67
+ // that second reason: at module top level its `process.exit(0)` exited every
68
+ // process that merely imported this hook.
69
+ if (isMainModule(import.meta.url)) {
70
+ if (!shouldRunHook('operator-steer')) process.exit(0);
71
+ main().catch(() => {}).finally(() => process.exit(0));
72
+ }
@@ -68,12 +68,13 @@ import {
68
68
  } from '../scripts/lib/issue-budget.mjs';
69
69
 
70
70
  import { shouldRunHook } from './_lib/profile-gate.mjs';
71
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
71
72
  import { existsSync } from 'node:fs';
72
73
  import path from 'node:path';
73
74
 
74
75
  // Opt-out per session via SO_DISABLED_HOOKS=post-bash-issue-budget-refund; the
75
- // "minimal"/"off" profiles disable it like every other non-core hook.
76
- if (!shouldRunHook('post-bash-issue-budget-refund')) process.exit(0);
76
+ // "minimal"/"off" profiles disable it like every other non-core hook — the gate
77
+ // itself runs inside the entry guard at the bottom of this file (#1393).
77
78
 
78
79
  // ---------------------------------------------------------------------------
79
80
  // Helpers
@@ -367,9 +368,18 @@ async function main() {
367
368
  }
368
369
 
369
370
  // Top-level error handler — fail open, same posture as the sibling hooks.
370
- main().catch((e) => {
371
- process.stderr.write(
372
- `⚠ post-bash-issue-budget-refund: internal error — ${e?.message || e}\n`,
373
- );
374
- process.exit(0);
375
- });
371
+ // Entry guard (#1393): run only when this file IS the script node was invoked
372
+ // with — every harness path execs it (`sh run-node.sh <this file>`). A bare
373
+ // `import()` (a probe, a test, a curious agent) must neither run main() nor
374
+ // tear the importing process down. The profile gate sits INSIDE the guard for
375
+ // that second reason: at module top level its `process.exit(0)` exited every
376
+ // process that merely imported this hook.
377
+ if (isMainModule(import.meta.url)) {
378
+ if (!shouldRunHook('post-bash-issue-budget-refund')) process.exit(0);
379
+ main().catch((e) => {
380
+ process.stderr.write(
381
+ `⚠ post-bash-issue-budget-refund: internal error — ${e?.message || e}\n`,
382
+ );
383
+ process.exit(0);
384
+ });
385
+ }
@@ -143,8 +143,6 @@
143
143
  */
144
144
 
145
145
  import { shouldRunHook } from './_lib/profile-gate.mjs';
146
- // Exit 0 immediately (silent no-op) when disabled via profile/env (#211).
147
- if (!shouldRunHook('post-bash-write-verify')) process.exit(0);
148
146
 
149
147
  import path from 'node:path';
150
148
  import os from 'node:os';
@@ -1136,6 +1134,9 @@ function invokedAsScript() {
1136
1134
  }
1137
1135
  const isMain = invokedAsScript();
1138
1136
  if (isMain) {
1137
+ // #1393: the profile gate sits INSIDE the entry guard — at module top level
1138
+ // its `process.exit(0)` exited every process that merely imported this hook.
1139
+ if (!shouldRunHook('post-bash-write-verify')) process.exit(0);
1139
1140
  // Advisory hook: never block, never surface an error to the tool call.
1140
1141
  main().catch(() => {}).finally(() => process.exit(0));
1141
1142
  }
@@ -37,8 +37,7 @@
37
37
  */
38
38
 
39
39
  import { shouldRunHook } from './_lib/profile-gate.mjs';
40
- // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
41
- if (!shouldRunHook('post-edit-import-probe')) process.exit(0);
40
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
42
41
 
43
42
  import path from 'node:path';
44
43
  import { readFileSync, existsSync } from 'node:fs';
@@ -335,10 +334,19 @@ async function main() {
335
334
  // Exit 0 always — a hook that guards against hook breakage must never block.
336
335
  // The catch stays silent towards the TOOL CALL but not towards the operator:
337
336
  // a probe whose own breakage is invisible is a probe nobody can trust.
338
- main()
339
- .catch((err) => {
340
- try {
341
- process.stderr.write(`post-edit-import-probe: ${err?.message ?? String(err)}\n`);
342
- } catch { /* stderr gone — nothing left to report to */ }
343
- })
344
- .finally(() => process.exit(0));
337
+ // Entry guard (#1393): run only when this file IS the script node was invoked
338
+ // with — every harness path execs it (`sh run-node.sh <this file>`). A bare
339
+ // `import()` (a probe, a test, a curious agent) must neither run main() nor
340
+ // tear the importing process down. The profile gate sits INSIDE the guard for
341
+ // that second reason: at module top level its `process.exit(0)` exited every
342
+ // process that merely imported this hook.
343
+ if (isMainModule(import.meta.url)) {
344
+ if (!shouldRunHook('post-edit-import-probe')) process.exit(0);
345
+ main()
346
+ .catch((err) => {
347
+ try {
348
+ process.stderr.write(`post-edit-import-probe: ${err?.message ?? String(err)}\n`);
349
+ } catch { /* stderr gone — nothing left to report to */ }
350
+ })
351
+ .finally(() => process.exit(0));
352
+ }
@@ -32,8 +32,7 @@ import { spawnSync } from 'node:child_process';
32
32
  import { performance } from 'node:perf_hooks';
33
33
 
34
34
  import { shouldRunHook } from './_lib/profile-gate.mjs';
35
- // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
36
- if (!shouldRunHook('post-edit-validate')) process.exit(0);
35
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
37
36
 
38
37
  import { readStdin } from '../scripts/lib/io.mjs';
39
38
  import { resolveProjectDir } from '../scripts/lib/platform.mjs';
@@ -220,6 +219,15 @@ async function main() {
220
219
  }
221
220
 
222
221
  // REQ-01: top-level catch — PostToolUse must never block; always exit 0
223
- main().catch(() => {
224
- process.exit(0);
225
- });
222
+ // Entry guard (#1393): run only when this file IS the script node was invoked
223
+ // with — every harness path execs it (`sh run-node.sh <this file>`). A bare
224
+ // `import()` (a probe, a test, a curious agent) must neither run main() nor
225
+ // tear the importing process down. The profile gate sits INSIDE the guard for
226
+ // that second reason: at module top level its `process.exit(0)` exited every
227
+ // process that merely imported this hook.
228
+ if (isMainModule(import.meta.url)) {
229
+ if (!shouldRunHook('post-edit-validate')) process.exit(0);
230
+ main().catch(() => {
231
+ process.exit(0);
232
+ });
233
+ }