session-orchestrator 5.1.0 → 5.3.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 (484) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +6 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +7 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +8 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +21 -0
  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 +7 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +7 -2
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +7 -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 +7 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +7 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +8 -3
  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 +22 -0
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +7 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +20 -0
  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 +6 -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/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +6 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +8 -2
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +21 -0
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +6 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +22 -0
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +6 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +6 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +7 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +21 -0
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +7 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  83. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  84. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  85. package/.codex-plugin/skills/close/SKILL.md +3 -3
  86. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
  87. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  88. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  89. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  90. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  91. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  92. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  93. package/.codex-plugin/skills/go/SKILL.md +3 -3
  94. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  95. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  96. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  97. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
  98. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  99. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  100. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  101. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  102. package/.codex-plugin/skills/release/SKILL.md +3 -3
  103. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  104. package/.codex-plugin/skills/session/SKILL.md +1 -1
  105. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  106. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  107. package/.codex-plugin/skills/test/SKILL.md +3 -3
  108. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  109. package/.cursor/commands/autopilot.md +4 -4
  110. package/.cursor/commands/bootstrap.md +5 -4
  111. package/.cursor/commands/brainstorm.md +5 -4
  112. package/.cursor/commands/close.md +4 -3
  113. package/.cursor/commands/debug.md +4 -4
  114. package/.cursor/commands/discovery.md +4 -4
  115. package/.cursor/commands/dispatcher.md +4 -4
  116. package/.cursor/commands/eli5.md +4 -4
  117. package/.cursor/commands/eval.md +4 -4
  118. package/.cursor/commands/evolve.md +4 -4
  119. package/.cursor/commands/go.md +4 -3
  120. package/.cursor/commands/grill.md +4 -4
  121. package/.cursor/commands/harness-audit.md +3 -3
  122. package/.cursor/commands/memory-cleanup.md +4 -4
  123. package/.cursor/commands/persona-panel.md +4 -4
  124. package/.cursor/commands/plan.md +5 -4
  125. package/.cursor/commands/portfolio.md +3 -3
  126. package/.cursor/commands/reconcile.md +4 -4
  127. package/.cursor/commands/release.md +4 -3
  128. package/.cursor/commands/repo-audit.md +4 -4
  129. package/.cursor/commands/session.md +1 -1
  130. package/.cursor/commands/spinout.md +4 -4
  131. package/.cursor/commands/sunset-review.md +4 -4
  132. package/.cursor/commands/test.md +3 -3
  133. package/.cursor/commands/ux-grill.md +4 -4
  134. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  135. package/.cursor/rules/010-session-workflow.mdc +2 -2
  136. package/.cursor/rules/050-plan.mdc +1 -1
  137. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  138. package/.cursor/skills/close/SKILL.md +13 -0
  139. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  140. package/.cursor/skills/debug/SKILL.md +0 -1
  141. package/.cursor/skills/discovery/SKILL.md +0 -1
  142. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  143. package/.cursor/skills/eli5/SKILL.md +0 -1
  144. package/.cursor/skills/eval/SKILL.md +1 -1
  145. package/.cursor/skills/evolve/SKILL.md +0 -1
  146. package/.cursor/skills/go/SKILL.md +13 -0
  147. package/.cursor/skills/grill/SKILL.md +0 -1
  148. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  149. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  150. package/.cursor/skills/portfolio/SKILL.md +12 -0
  151. package/.cursor/skills/release/SKILL.md +13 -0
  152. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  153. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  154. package/.cursor/skills/test/SKILL.md +12 -0
  155. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  156. package/.cursor-plugin/plugin.json +1 -1
  157. package/.orchestrator/policy/blocked-commands.json +13 -4
  158. package/AGENTS.md +3 -2
  159. package/CHANGELOG.md +197 -0
  160. package/README.md +11 -9
  161. package/SECURITY.md +12 -0
  162. package/agents/dialectic-deriver.md +13 -10
  163. package/agents/eval-judge.md +67 -45
  164. package/agents/skill-applied-judge.md +34 -19
  165. package/commands/session.md +17 -3
  166. package/docs/baseline.md +12 -6
  167. package/docs/ci-setup.md +53 -0
  168. package/docs/codex-setup.md +15 -3
  169. package/docs/components.md +13 -6
  170. package/docs/events-schema.md +59 -9
  171. package/docs/install.md +16 -0
  172. package/docs/persona-panel.md +1 -1
  173. package/docs/pi-setup.md +1 -1
  174. package/docs/rule-authoring.md +135 -14
  175. package/docs/scope-collision-guard.md +2 -0
  176. package/docs/session-config-reference.md +106 -11
  177. package/docs/session-config-template.md +31 -2
  178. package/docs/telemetry.md +2 -0
  179. package/hooks/_lib/hook-import-set.json +125 -8
  180. package/hooks/_lib/subagent-paths.mjs +15 -0
  181. package/hooks/_lib/subagent-transcript.mjs +582 -31
  182. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  183. package/hooks/config-protection.mjs +11 -3
  184. package/hooks/cwd-change-restore.mjs +11 -3
  185. package/hooks/enforce-commands.mjs +70 -23
  186. package/hooks/enforce-scope.mjs +143 -33
  187. package/hooks/hooks-codex.json +1 -1
  188. package/hooks/hooks.json +1 -1
  189. package/hooks/loop-guard.mjs +11 -3
  190. package/hooks/on-session-end.mjs +72 -25
  191. package/hooks/on-session-start.mjs +48 -11
  192. package/hooks/on-stop.mjs +211 -23
  193. package/hooks/operator-steer.mjs +11 -3
  194. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  195. package/hooks/post-bash-write-verify.mjs +6 -2
  196. package/hooks/post-edit-import-probe.mjs +17 -9
  197. package/hooks/post-edit-validate.mjs +13 -5
  198. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  199. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  200. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  201. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  202. package/hooks/pre-auq-clarity.mjs +18 -2
  203. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  204. package/hooks/pre-bash-issue-budget.mjs +119 -28
  205. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  206. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  207. package/hooks/pre-bash-staging-fence.mjs +335 -31
  208. package/hooks/pre-bash-templates-first.mjs +19 -14
  209. package/hooks/pre-task-scope-disjoint.mjs +385 -5
  210. package/hooks/skill-invocation-telemetry.mjs +2 -1
  211. package/hooks/subagent-telemetry.mjs +15 -19
  212. package/hooks/wave-scope-commit-guard.mjs +197 -100
  213. package/monitors/monitors.json +1 -1
  214. package/output-styles/wave-summary.md +1 -1
  215. package/package.json +2 -1
  216. package/pi/prompts/autopilot.md +3 -3
  217. package/pi/prompts/bootstrap.md +3 -3
  218. package/pi/prompts/brainstorm.md +3 -3
  219. package/pi/prompts/close.md +2 -2
  220. package/pi/prompts/debug.md +3 -3
  221. package/pi/prompts/discovery.md +3 -3
  222. package/pi/prompts/dispatcher.md +3 -3
  223. package/pi/prompts/eli5.md +3 -3
  224. package/pi/prompts/eval.md +3 -3
  225. package/pi/prompts/evolve.md +3 -3
  226. package/pi/prompts/go.md +2 -2
  227. package/pi/prompts/grill.md +3 -3
  228. package/pi/prompts/harness-audit.md +2 -3
  229. package/pi/prompts/memory-cleanup.md +3 -3
  230. package/pi/prompts/persona-panel.md +3 -3
  231. package/pi/prompts/plan.md +3 -3
  232. package/pi/prompts/portfolio.md +2 -2
  233. package/pi/prompts/reconcile.md +3 -3
  234. package/pi/prompts/release.md +3 -3
  235. package/pi/prompts/repo-audit.md +3 -4
  236. package/pi/prompts/session.md +2 -2
  237. package/pi/prompts/spinout.md +3 -3
  238. package/pi/prompts/sunset-review.md +3 -3
  239. package/pi/prompts/templates-ack.md +1 -1
  240. package/pi/prompts/test.md +3 -3
  241. package/pi/prompts/ux-grill.md +3 -3
  242. package/rules/README.md +1 -1
  243. package/rules/opt-in-domain/prompt-caching.md +1 -1
  244. package/rules/opt-in-stack/backend-data.md +1 -1
  245. package/rules/opt-in-stack/backend.md +3 -3
  246. package/rules/opt-in-stack/frontend.md +1 -1
  247. package/rules/opt-in-stack/security-web.md +3 -3
  248. package/rules/opt-in-stack/swift.md +1 -1
  249. package/scripts/archive-closed-prds.mjs +2 -2
  250. package/scripts/auq-audit.mjs +2 -3
  251. package/scripts/autopilot.mjs +23 -2
  252. package/scripts/backfill-abandoned-sessions.mjs +171 -15
  253. package/scripts/backfill-evidence-digest.mjs +2 -1
  254. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  255. package/scripts/check-package-manager.mjs +2 -2
  256. package/scripts/check-sessions-integrity.mjs +300 -0
  257. package/scripts/ci/assert-vitest-green.mjs +2 -1
  258. package/scripts/dialectic-deriver.mjs +50 -13
  259. package/scripts/emit-session.mjs +77 -32
  260. package/scripts/eval-session.mjs +65 -3
  261. package/scripts/export-hw-learnings.mjs +2 -1
  262. package/scripts/express-path.mjs +1 -1
  263. package/scripts/gc-stale-worktrees.mjs +2 -1
  264. package/scripts/generate-agents-skills.mjs +102 -29
  265. package/scripts/generate-codex-skills.mjs +48 -4
  266. package/scripts/generate-cursor-adapter.mjs +220 -11
  267. package/scripts/generate-hook-import-set.mjs +12 -27
  268. package/scripts/generate-pi-prompts.mjs +183 -13
  269. package/scripts/github-protection-audit.mjs +2 -3
  270. package/scripts/lib/agent-frontmatter.mjs +23 -1
  271. package/scripts/lib/agent-status.mjs +2 -31
  272. package/scripts/lib/auq/clarity.mjs +10 -2
  273. package/scripts/lib/auq/parse.mjs +12 -31
  274. package/scripts/lib/auq/schema.mjs +56 -41
  275. package/scripts/lib/auto-dialectic.mjs +304 -15
  276. package/scripts/lib/autopilot/flags.mjs +12 -1
  277. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  278. package/scripts/lib/autopilot/loop.mjs +14 -1
  279. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  280. package/scripts/lib/ci-status-banner.mjs +376 -16
  281. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  282. package/scripts/lib/command-blocker.mjs +408 -33
  283. package/scripts/lib/config/dialectic.mjs +12 -3
  284. package/scripts/lib/config/drift-check.mjs +19 -0
  285. package/scripts/lib/config/gate.mjs +74 -0
  286. package/scripts/lib/config/reaper.mjs +162 -0
  287. package/scripts/lib/config.mjs +14 -0
  288. package/scripts/lib/convergence-monitor.mjs +76 -13
  289. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  290. package/scripts/lib/description-surface.mjs +2 -5
  291. package/scripts/lib/dispatcher/cli.mjs +2 -1
  292. package/scripts/lib/ecosystem-health.mjs +11 -0
  293. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  294. package/scripts/lib/eval/engine.mjs +421 -53
  295. package/scripts/lib/eval/judge.mjs +463 -40
  296. package/scripts/lib/eval/schema.mjs +10 -1
  297. package/scripts/lib/events-rotation.mjs +221 -25
  298. package/scripts/lib/events-schema.mjs +114 -0
  299. package/scripts/lib/events.mjs +524 -5
  300. package/scripts/lib/fetch-baseline.mjs +3 -8
  301. package/scripts/lib/frontmatter-guard.mjs +21 -10
  302. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  303. package/scripts/lib/gates/gate-full.mjs +28 -3
  304. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  305. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  306. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  307. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  308. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  309. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  310. package/scripts/lib/instruction-budget-guard.mjs +332 -50
  311. package/scripts/lib/io.mjs +42 -8
  312. package/scripts/lib/is-main-module.mjs +82 -0
  313. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  314. package/scripts/lib/js-mask.mjs +197 -0
  315. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  316. package/scripts/lib/locks/index.mjs +32 -25
  317. package/scripts/lib/maintenance-due-banner.mjs +122 -91
  318. package/scripts/lib/orphan-reaper.mjs +1588 -0
  319. package/scripts/lib/peer-cards/merger.mjs +48 -10
  320. package/scripts/lib/peer-cards/reader.mjs +78 -2
  321. package/scripts/lib/peer-discovery.mjs +2 -5
  322. package/scripts/lib/playwright-driver/runner.mjs +2 -1
  323. package/scripts/lib/process-group.mjs +899 -0
  324. package/scripts/lib/quality-gate.mjs +107 -28
  325. package/scripts/lib/reconcile/backlog.mjs +368 -0
  326. package/scripts/lib/reconcile/engine.mjs +55 -188
  327. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
  328. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  329. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  330. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  331. package/scripts/lib/rule-loader.mjs +41 -12
  332. package/scripts/lib/rules-sync.mjs +2 -5
  333. package/scripts/lib/scope-echo.mjs +429 -7
  334. package/scripts/lib/scope-gate.mjs +605 -1
  335. package/scripts/lib/session-close-backfill.mjs +91 -12
  336. package/scripts/lib/session-id.mjs +9 -20
  337. package/scripts/lib/session-invocation.mjs +20 -0
  338. package/scripts/lib/session-schema/constants.mjs +30 -2
  339. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  340. package/scripts/lib/session-schema.mjs +8 -3
  341. package/scripts/lib/session-start-probes.mjs +95 -10
  342. package/scripts/lib/sessions-canonical.mjs +23 -0
  343. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  344. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  345. package/scripts/lib/skill-evidence-window.mjs +891 -0
  346. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  347. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  348. package/scripts/lib/skill-judge.mjs +45 -3
  349. package/scripts/lib/state-md.mjs +84 -3
  350. package/scripts/lib/sunset/walker.mjs +31 -4
  351. package/scripts/lib/tail-window.mjs +56 -0
  352. package/scripts/lib/telemetry/schema.mjs +30 -0
  353. package/scripts/lib/telemetry/sync.mjs +61 -6
  354. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  355. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  356. package/scripts/lib/tests-src-ratio.mjs +2 -6
  357. package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
  358. package/scripts/lib/user-invocable-skills.mjs +205 -0
  359. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  360. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  361. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  362. package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
  363. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  364. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  365. package/scripts/lib/validate/check-entry-guard.mjs +329 -0
  366. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  367. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  368. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  369. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  370. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  371. package/scripts/lib/validate/check-rules.mjs +7 -5
  372. package/scripts/lib/validate/check-skill-links.mjs +35 -6
  373. package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
  374. package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
  375. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  376. package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
  377. package/scripts/lib/validate/check-unwired-features.mjs +220 -33
  378. package/scripts/lib/validate/check-validator-registration.mjs +36 -12
  379. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  380. package/scripts/lib/validate/confidential-names.mjs +10 -0
  381. package/scripts/lib/validate-vendored-rules.mjs +39 -12
  382. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  383. package/scripts/lib/vault-mirror/process.mjs +10 -3
  384. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  385. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  386. package/scripts/lib/vault-yaml.mjs +118 -0
  387. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  388. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  389. package/scripts/lock-reaper.mjs +2 -1
  390. package/scripts/materialize-wave-scope.mjs +87 -4
  391. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  392. package/scripts/migrate-vault-paths.mjs +2 -3
  393. package/scripts/release-session-lock.mjs +305 -0
  394. package/scripts/release.mjs +109 -39
  395. package/scripts/relocate-vault-corpus.mjs +2 -3
  396. package/scripts/repair-invalid-sessions.mjs +2 -2
  397. package/scripts/resolve-session-invocation.mjs +59 -0
  398. package/scripts/run-quality-gate.mjs +156 -17
  399. package/scripts/session-shape.mjs +2 -2
  400. package/scripts/site-numbers.mjs +35 -11
  401. package/scripts/sweep-expired-rules.mjs +227 -0
  402. package/scripts/validate-plugin.mjs +21 -0
  403. package/scripts/validate-wave-scope.mjs +32 -105
  404. package/scripts/vault-consolidate.mjs +2 -2
  405. package/scripts/vault-mirror.mjs +11 -4
  406. package/scripts/wave-scope-binding.mjs +2 -3
  407. package/skills/_shared/bootstrap-gate.md +1 -1
  408. package/skills/_shared/monitor-patterns.md +1 -1
  409. package/skills/_shared/platform-tools.md +23 -11
  410. package/skills/_shared/research-evidence.md +53 -0
  411. package/skills/_shared/state-ownership.md +3 -0
  412. package/skills/autopilot/SKILL.md +80 -11
  413. package/skills/bootstrap/SKILL.md +51 -1
  414. package/skills/brainstorm/SKILL.md +16 -0
  415. package/skills/claude-md-drift-check/SKILL.md +1 -1
  416. package/skills/claude-md-drift-check/checker.mjs +49 -11
  417. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  418. package/skills/convergence-monitoring/README.md +8 -1
  419. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  420. package/skills/convergence-monitoring/SKILL.md +15 -6
  421. package/skills/debug/SKILL.md +10 -0
  422. package/skills/discovery/SKILL.md +24 -1
  423. package/skills/discovery/probes-session.md +2 -2
  424. package/skills/dispatcher/SKILL.md +38 -7
  425. package/skills/eli5/SKILL.md +11 -0
  426. package/skills/eval/SKILL.md +52 -23
  427. package/skills/eval/rubric-v1.md +1 -0
  428. package/skills/eval/rubric-v2.md +457 -0
  429. package/skills/evolve/SKILL.md +9 -2
  430. package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
  431. package/skills/gitlab-ops/SKILL.md +3 -2
  432. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  433. package/skills/grill/SKILL.md +19 -0
  434. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  435. package/skills/hook-development/SKILL.md +46 -41
  436. package/skills/memory-cleanup/SKILL.md +7 -0
  437. package/skills/npm-publish/SKILL.md +2 -2
  438. package/skills/persona-panel/SKILL.md +56 -1
  439. package/skills/persona-panel/persona-format.md +1 -1
  440. package/skills/plan/SKILL.md +28 -1
  441. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  442. package/skills/reconcile/SKILL.md +21 -0
  443. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  444. package/skills/repo-audit/SKILL.md +7 -0
  445. package/skills/session-end/SKILL.md +13 -16
  446. package/skills/session-end/discovery-scan.md +1 -1
  447. package/skills/session-end/phase-3-6-tail.md +55 -9
  448. package/skills/session-end/plan-verification.md +2 -2
  449. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  450. package/skills/session-end/session-metrics-write.md +10 -0
  451. package/skills/session-plan/SKILL.md +18 -6
  452. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  453. package/skills/session-start/SKILL.md +5 -4
  454. package/skills/session-start/phase-8-5-express-path.md +6 -6
  455. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  456. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  457. package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
  458. package/skills/spinout/SKILL.md +12 -1
  459. package/skills/sunset-review/SKILL.md +13 -0
  460. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  461. package/skills/ux-grill/SKILL.md +20 -2
  462. package/skills/wave-executor/SKILL.md +14 -7
  463. package/skills/wave-executor/circuit-breaker.md +2 -0
  464. package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
  465. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  466. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  467. package/commands/autopilot.md +0 -80
  468. package/commands/bootstrap.md +0 -56
  469. package/commands/brainstorm.md +0 -48
  470. package/commands/debug.md +0 -36
  471. package/commands/discovery.md +0 -32
  472. package/commands/dispatcher.md +0 -59
  473. package/commands/eli5.md +0 -33
  474. package/commands/eval.md +0 -28
  475. package/commands/evolve.md +0 -10
  476. package/commands/grill.md +0 -45
  477. package/commands/memory-cleanup.md +0 -26
  478. package/commands/persona-panel.md +0 -121
  479. package/commands/plan.md +0 -15
  480. package/commands/reconcile.md +0 -23
  481. package/commands/repo-audit.md +0 -24
  482. package/commands/spinout.md +0 -15
  483. package/commands/sunset-review.md +0 -27
  484. package/commands/ux-grill.md +0 -51
@@ -21,7 +21,10 @@
21
21
  * matches the fully realpath-resolved candidate → allow, BEFORE G6.
22
22
  * Runs before G6 so a deliberate out-of-repo grant (e.g. a vault path)
23
23
  * is reachable at all — G6 would otherwise deny every out-of-repo path
24
- * without ever consulting allowedPaths. See matchesAbsoluteAllowlist.
24
+ * without ever consulting allowedPaths. See matchedAbsoluteGrant.
25
+ * (#1398 cond. 4) The matched grant is GRADED through the shared
26
+ * `gradeScopeEntry` predicate; an `error` verdict emits ONE WARN and the
27
+ * write is still ALLOWED — this gate never denies on a grading verdict.
25
28
  * G5c (#1295) out-of-root carveout for THIS repo's Claude Code auto-memory
26
29
  * directory (`~/.claude/projects/<encoded-repo-path>/memory/`). Harness-
27
30
  * owned, lives outside the working copy, cannot collide with any wave
@@ -68,8 +71,11 @@ import { promises as fs } from 'node:fs';
68
71
  import { pathToFileURL } from 'node:url';
69
72
 
70
73
  import { shouldRunHook } from './_lib/profile-gate.mjs';
71
- // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
72
- if (!shouldRunHook('enforce-scope')) process.exit(0);
74
+ // Static for the SAME reason profile-gate.mjs is (#993, see the late-binding
75
+ // block below): a leaf predicate with ZERO repo imports (node:fs + node:url
76
+ // only) that decides whether this hook runs at all. Everything carrying a
77
+ // transitive repo graph stays late-bound inside bootstrap().
78
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
73
79
 
74
80
  // ---------------------------------------------------------------------------
75
81
  // #993 — late-bound repo dependencies
@@ -109,6 +115,12 @@ let readJson;
109
115
  let classifyEmptyScope;
110
116
  let suggestForEmptyScope;
111
117
  let sessionStartedAtMs;
118
+ // #1398 cond. 4 — THE shared grading predicate for an absolute Gate 5b grant,
119
+ // plus the resolver it takes. The hook WARNS on an `error` verdict and never
120
+ // denies on it; see Gate 5b. Both come from the module already bound below —
121
+ // no new module edge, so `hooks/_lib/hook-import-set.json` is unchanged.
122
+ let gradeScopeEntry;
123
+ let canonicalizeGrantPrefix;
112
124
  // #1123 — "is this manifest even mine?" (G3b). Process-local identity only
113
125
  // (#1194): the repo-global `session.lock` tier is shared by every session in the
114
126
  // checkout and would classify a peer's manifest as ours.
@@ -188,7 +200,13 @@ async function bootstrap() {
188
200
  ({ resolveProjectDir } = modules.platform);
189
201
  ({ findScopeFile, pathMatchesPattern, suggestForScopeViolation } = modules.hardening);
190
202
  ({ readJson } = modules.common);
191
- ({ classifyEmptyScope, suggestForEmptyScope, sessionStartedAtMs } = modules.scopeGate);
203
+ ({
204
+ classifyEmptyScope,
205
+ suggestForEmptyScope,
206
+ sessionStartedAtMs,
207
+ gradeScopeEntry,
208
+ canonicalizeGrantPrefix,
209
+ } = modules.scopeGate);
192
210
  ({ readProcessLocalSessionIds, classifyManifestSession } = modules.sessionIdentity);
193
211
  }
194
212
 
@@ -433,7 +451,52 @@ async function main() {
433
451
  // out-of-repo path before allowedPaths is ever consulted. Honour such grants
434
452
  // here — matching ONLY absolute entries against the fully realpath-resolved
435
453
  // candidate, so relative entries can never be used to escape the repo (REQ-09).
436
- if (matchesAbsoluteAllowlist(resolvedPath, allowedPaths)) return emitAllow();
454
+ //
455
+ // GRADING (#1398 acceptance condition 4): this gate ALLOWS exactly what it
456
+ // allowed before — the verdict below is a WARN, never a deny. Until now the
457
+ // asymmetry was silent: `scripts/validate-wave-scope.mjs` graded these grants
458
+ // (system-root denylist, home depth + sensitive-subdirectory rule) and NOTHING
459
+ // called it from code — four prose steps in `skills/wave-executor/` were the
460
+ // only thing between a manifest and this gate, so a manifest that skipped them
461
+ // reached here ungraded and the operator never learned that it had.
462
+ //
463
+ // Now the same predicate runs HERE, on the grant that actually matched, and an
464
+ // `error` verdict surfaces as one operator-visible notice. Deliberately not a
465
+ // deny: this hook runs in every repo on the host, the grant is the
466
+ // coordinator's own artefact, and turning a live allow into a block on a
467
+ // pre-flight rule would break working sessions to enforce a policy whose place
468
+ // is before dispatch. `emitWarn` is an ALLOW that carries a notice (exit 0,
469
+ // `systemMessage` only) — see `scripts/lib/io.mjs`.
470
+ //
471
+ // THE SAME `resolve` THE VALIDATOR PASSES (#1398 cond. 4, closed 2026-09-21).
472
+ // It was omitted here on the assumption that canonicalisation is too expensive
473
+ // for a PreToolUse hot path; measuring it refuted that, and the omission was
474
+ // the last source of a verdict divergence between the two callers of one
475
+ // predicate (`/tmp/x/**`: `warn` here, `error/non-canonical` there — silent on
476
+ // exactly the grant that matches nothing at this gate).
477
+ //
478
+ // MEASURED 2026-09-21, A/B in one process, 200 repetitions, median:
479
+ // - this call site grades ONE grant per Gate 5b hit, never the manifest:
480
+ // +0.067 ms, 15 `realpathSync` — 75× under the 5 ms decision threshold;
481
+ // - a Gate 5b hit requires an out-of-repo write AND an absolute grant that
482
+ // matches it. The live 64-entry manifest of that session carried 0
483
+ // absolute entries and therefore cost 0 syscalls: an ordinary in-repo
484
+ // write never reaches this line at all.
485
+ // `canonicalizeGrantPrefix` memoizes per process and never throws (see its
486
+ // docblock); `gradeScopeEntry` additionally degrades a throwing resolver to
487
+ // the literal spelling, so the worst case is today's verdict.
488
+ const matchedGrant = matchedAbsoluteGrant(resolvedPath, allowedPaths);
489
+ if (matchedGrant !== null) {
490
+ const grade = gradeScopeEntry(matchedGrant, { resolve: canonicalizeGrantPrefix });
491
+ if (grade?.verdict === 'error') {
492
+ return emitWarn(
493
+ `Gate 5b honoured an out-of-repo grant that scripts/validate-wave-scope.mjs would REFUSE ` +
494
+ `before dispatch — allowedPaths ${grade.message}. The write to '${resolvedPath}' is ` +
495
+ `ALLOWED (this gate never denies on a grading verdict); re-validate the manifest.`,
496
+ );
497
+ }
498
+ return emitAllow();
499
+ }
437
500
 
438
501
  // Gate 6: path must be inside the project root
439
502
  if (!isPathInside(resolvedPath, projectRoot)) {
@@ -774,19 +837,54 @@ function isCoordinatorCarveout(normalizedRel, projectRoot, scopePath) {
774
837
  * SECURITY (REQ-09): matches ONLY entries that are themselves absolute, against
775
838
  * the fully realpath-resolved candidate — so RELATIVE entries (`**`, `../**`)
776
839
  * can NEVER be used to escape the repo. When no absolute entry exists the helper
777
- * returns false and the caller falls through to Gate 6 unchanged (inert pre-gate).
840
+ * returns null and the caller falls through to Gate 6 unchanged (inert pre-gate).
778
841
  * An absolute entry matches only its own literal (canonical/realpath) subtree;
779
842
  * the operator is responsible for supplying a canonical absolute path.
780
843
  *
844
+ * MATCHING IS SHAPE-BLIND HERE; GRADING IS SHARED (#1398 cond. 4). This helper
845
+ * still decides nothing about whether a grant SHOULD exist: `/Users/<u>/**` and
846
+ * `/Users/<u>/.ssh/**` match exactly like `/Users/<u>/Projects/vault/**`. What
847
+ * changed is that the CALLER now runs the ONE grading predicate —
848
+ * `gradeScopeEntry` in `scripts/lib/scope-gate.mjs`, the same function
849
+ * `scripts/validate-wave-scope.mjs` turns into an exit-1 refusal — over the grant
850
+ * this helper returns, and WARNS when it grades `error`.
851
+ *
852
+ * ONE PREDICATE, ONE ARGUMENT SHAPE — THE VERDICTS NO LONGER DIFFER (#1398
853
+ * cond. 4, closed 2026-09-21). Both callers now pass the SAME fs-backed
854
+ * `resolve` (`canonicalizeGrantPrefix`, exported from
855
+ * `scripts/lib/scope-gate.mjs`), so only the CONSEQUENCE is asymmetric: the
856
+ * validator refuses an `error` before dispatch, this gate allows it with a
857
+ * notice at write time. The history, because the asymmetry was load-bearing for
858
+ * two fixes, measured 2026-09-20 @ 7e110a2a with the resolver-free call:
859
+ * - `/private/etc/**`, `/private/var/**` — the macOS realpaths of the
860
+ * denylisted `/etc` and `/var`, i.e. the spellings Gate 5b ACTUALLY matches
861
+ * (it matches the realpath-resolved candidate): hook `warn`, validator
862
+ * `error`. The notice fired on `/etc/**`, which reaches nothing here, and
863
+ * stayed silent on the spelling that reaches everything. CLOSED by listing
864
+ * the two aliases literally (`DENIED_ABSOLUTE_ALIAS_ROOTS`), at zero
865
+ * syscalls — they still cost nothing and stay.
866
+ * - `/tmp/x/**` — hook `warn`, validator `error/non-canonical`: the last
867
+ * divergence, 1 of 9 probes, and not closable by any literal list, because
868
+ * proving a literal prefix resolves elsewhere IS the realpath call. CLOSED
869
+ * by passing the resolver here too, once its cost was measured instead of
870
+ * assumed (+0.067 ms per Gate 5b hit; see the call site).
871
+ * The parity of the two verdict tables is pinned by
872
+ * `tests/lib/scope-gate.test.mjs` § "hook / validator grading parity", so a NEW
873
+ * divergence is a red test rather than a comment someone has to notice.
874
+ *
875
+ * Returns the MATCHED PATTERN rather than a boolean precisely so the caller has
876
+ * something to grade: with a bare `true` the grant that opened the gate is
877
+ * unknowable at the call site, and any notice would have to re-derive it.
878
+ *
781
879
  * @param {string} resolvedPath — fully realpath-resolved candidate (absolute)
782
880
  * @param {string[]} allowedPaths — raw allowedPaths array from wave-scope.json
783
- * @returns {boolean}
881
+ * @returns {string|null} the first matching absolute entry, or null
784
882
  */
785
- function matchesAbsoluteAllowlist(resolvedPath, allowedPaths) {
883
+ function matchedAbsoluteGrant(resolvedPath, allowedPaths) {
786
884
  const abs = allowedPaths.filter((p) => typeof p === 'string' && path.isAbsolute(p));
787
- if (abs.length === 0) return false;
885
+ if (abs.length === 0) return null;
788
886
  const normalizedAbs = resolvedPath.split(path.sep).join('/');
789
- return abs.some((pat) => pathMatchesPattern(normalizedAbs, pat.split(path.sep).join('/')));
887
+ return abs.find((pat) => pathMatchesPattern(normalizedAbs, pat.split(path.sep).join('/'))) ?? null;
790
888
  }
791
889
 
792
890
  // ---------------------------------------------------------------------------
@@ -804,29 +902,41 @@ function matchesAbsoluteAllowlist(resolvedPath, allowedPaths) {
804
902
  // guard armed and then tripped over a specific path; that fails CLOSED via
805
903
  // emitDeny (SECURITY-REQ-01). The two paths MUST stay separate.
806
904
  // ---------------------------------------------------------------------------
807
- try {
808
- await bootstrap();
809
- } catch (loadError) {
905
+ // Entry guard (#1393): run only when this file IS the script node was invoked
906
+ // with — every harness path execs it (`sh run-node.sh <this file>`). A bare
907
+ // `import()` (a probe, a test, a curious agent) must neither run main() nor
908
+ // tear the importing process down. The profile gate sits INSIDE the guard for
909
+ // that second reason: at module top level its `process.exit(0)` exited every
910
+ // process that merely imported this hook. bootstrap() is inside too — loading
911
+ // the guard sources is work a disabled hook and a bare importer must not do.
912
+ if (isMainModule(import.meta.url)) {
913
+ // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
914
+ if (!shouldRunHook('enforce-scope')) process.exit(0);
915
+
810
916
  try {
811
- const { emitGuardInactiveBanner } = await import('./_lib/guard-source-loader.mjs');
812
- // hookName is threaded explicitly (#993 — no hard-wired literal in the loader).
813
- emitGuardInactiveBanner({ hookName: HOOK_NAME, error: loadError, consequence: GUARD_CONSEQUENCE });
814
- } catch {
815
- // Last resort: even the banner helper failed to load. Emit unconditionally
816
- // repeated noise beats a silent disarm.
817
- process.stderr.write(
818
- '🚨 enforce-scope: GUARD INACTIVE module load failed ' +
819
- `(${String(loadError?.message || loadError).split('\n')[0]}). ` +
820
- 'Edit/Write/MultiEdit scope enforcement is OFF. See issue #993.\n'
821
- );
917
+ await bootstrap();
918
+ } catch (loadError) {
919
+ try {
920
+ const { emitGuardInactiveBanner } = await import('./_lib/guard-source-loader.mjs');
921
+ // hookName is threaded explicitly (#993 no hard-wired literal in the loader).
922
+ emitGuardInactiveBanner({ hookName: HOOK_NAME, error: loadError, consequence: GUARD_CONSEQUENCE });
923
+ } catch {
924
+ // Last resort: even the banner helper failed to load. Emit unconditionally
925
+ // repeated noise beats a silent disarm.
926
+ process.stderr.write(
927
+ '🚨 enforce-scope: GUARD INACTIVE — module load failed ' +
928
+ `(${String(loadError?.message || loadError).split('\n')[0]}). ` +
929
+ 'Edit/Write/MultiEdit scope enforcement is OFF. See issue #993.\n'
930
+ );
931
+ }
932
+ process.exit(0); // fail-open, but no longer fail-silent
822
933
  }
823
- process.exit(0); // fail-open, but no longer fail-silent
824
- }
825
934
 
826
- // SECURITY-REQ-01 (fail-closed): any unhandled rejection → structured deny, never bare exit 1
827
- main().catch((e) => {
828
- emitDeny(
829
- 'Internal hook error — request blocked for safety',
830
- `${e?.message ?? String(e)}`,
831
- );
832
- });
935
+ // SECURITY-REQ-01 (fail-closed): any unhandled rejection → structured deny, never bare exit 1
936
+ main().catch((e) => {
937
+ emitDeny(
938
+ 'Internal hook error — request blocked for safety',
939
+ `${e?.message ?? String(e)}`,
940
+ );
941
+ });
942
+ }
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "echo '🎯 Session Orchestrator v5.1.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
+ "command": "echo '🎯 Session Orchestrator v5.3.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
11
11
  "async": false
12
12
  },
13
13
  {
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "echo '🎯 Session Orchestrator v5.1.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v5.3.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {
@@ -25,8 +25,7 @@
25
25
  */
26
26
 
27
27
  import { shouldRunHook } from './_lib/profile-gate.mjs';
28
- // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
29
- if (!shouldRunHook('loop-guard')) process.exit(0);
28
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
30
29
 
31
30
  import { promises as fs } from 'node:fs';
32
31
  import os from 'node:os';
@@ -257,4 +256,13 @@ async function main() {
257
256
  }
258
257
 
259
258
  // Exit 0 always — informational hook must never block Claude (#619).
260
- main().catch(() => {}).finally(() => process.exit(0));
259
+ // Entry guard (#1393): run only when this file IS the script node was invoked
260
+ // with — every harness path execs it (`sh run-node.sh <this file>`). A bare
261
+ // `import()` (a probe, a test, a curious agent) must neither run main() nor
262
+ // tear the importing process down. The profile gate sits INSIDE the guard for
263
+ // that second reason: at module top level its `process.exit(0)` exited every
264
+ // process that merely imported this hook.
265
+ if (isMainModule(import.meta.url)) {
266
+ if (!shouldRunHook('loop-guard')) process.exit(0);
267
+ main().catch(() => {}).finally(() => process.exit(0));
268
+ }
@@ -31,8 +31,6 @@ import { promises as fs } from 'node:fs';
31
31
  import path from 'node:path';
32
32
 
33
33
  import { shouldRunHook } from './_lib/profile-gate.mjs';
34
- // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
35
- if (!shouldRunHook('on-session-end')) process.exit(0);
36
34
 
37
35
  import { emitEvent } from '../scripts/lib/events.mjs';
38
36
  import { getProjectDir } from '../scripts/lib/platform.mjs';
@@ -47,6 +45,7 @@ import {
47
45
  OWNER_PROOF_RELPATH,
48
46
  } from '../scripts/lib/session-lock.mjs';
49
47
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
48
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
50
49
  import { deregisterSelf, logSweepEvent } from '../scripts/lib/session-registry.mjs';
51
50
  import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
52
51
  import { flush } from '../scripts/lib/telemetry/sync.mjs';
@@ -255,21 +254,44 @@ async function readPersistence(projectRoot) {
255
254
  }
256
255
 
257
256
  /**
258
- * Reduce a `flush()` result to the two-field breadcrumb the event carries.
259
- *
260
- * `reason` is normalised to its head token because `flush()` may return
261
- * `build-error: <message>`, and a raw error message is unbounded free text in a
262
- * stream whose whole purpose is aggregation by class.
257
+ * Characters kept from `flush()`'s raw `reason` in the event record.
258
+ *
259
+ * NAMED CEILING (BV-004): `flush()` may return `build-error: <message>`, i.e.
260
+ * unbounded free text, into a stream whose purpose is aggregation. 200 mirrors
261
+ * the sibling bound in `scripts/lib/session-start-probes.mjs`; the banner
262
+ * re-bounds to 120 and strips control bytes on its own side, because the ledger
263
+ * is an untrusted string source for anything that renders it.
264
+ * REVISIT TRIGGER: a legitimate `flush()` reason longer than 200 characters.
265
+ */
266
+ const FLUSH_REASON_MAX_CHARS = 200;
267
+
268
+ /**
269
+ * Reduce a `flush()` result to the three-field breadcrumb the event carries.
270
+ *
271
+ * `reason` is the FULL reason (length-bounded, see FLUSH_REASON_MAX_CHARS) and
272
+ * `reason_class` is its head token — the part before the first `:` — so an
273
+ * aggregation by class stays possible without destroying the detail.
274
+ *
275
+ * #1392, the defect this shape replaces: `reason` used to BE the head token
276
+ * (`String(res?.reason ?? 'unknown').split(':')[0]`). All five sandbox
277
+ * refusals `scripts/lib/telemetry/sync.mjs` produces (`sandbox:temp-root`,
278
+ * `sandbox:probe-failed`, …) were therefore written as bare `sandbox`, while
279
+ * the one reader — `scripts/lib/telemetry-flush-health-banner.mjs` — requires
280
+ * `reason.startsWith('sandbox:')`. The banner could not fire on any record this
281
+ * emitter ever wrote, and a mute instrument looks exactly like a healthy
282
+ * channel (HR-105). The cut is irrecoverable at the reader, so the fix belongs
283
+ * here: persist the full reason, carry the class beside it.
263
284
  *
264
285
  * @param {{sent?: boolean, queued?: boolean, reason?: string}|null|undefined} res
265
- * @returns {{outcome: 'sent'|'queued'|'gated'|'skipped', reason: string}}
286
+ * @returns {{outcome: 'sent'|'queued'|'gated'|'skipped', reason: string, reason_class: string}}
266
287
  */
267
- function classifyFlush(res) {
268
- const reason = String(res?.reason ?? 'unknown').split(':')[0];
269
- if (res?.sent === true) return { outcome: 'sent', reason };
270
- if (res?.queued === true) return { outcome: 'queued', reason };
271
- if (reason === 'gated') return { outcome: 'gated', reason };
272
- return { outcome: 'skipped', reason };
288
+ export function classifyFlush(res) {
289
+ const reason = String(res?.reason ?? 'unknown').slice(0, FLUSH_REASON_MAX_CHARS);
290
+ const reasonClass = reason.split(':')[0];
291
+ if (res?.sent === true) return { outcome: 'sent', reason, reason_class: reasonClass };
292
+ if (res?.queued === true) return { outcome: 'queued', reason, reason_class: reasonClass };
293
+ if (reasonClass === 'gated') return { outcome: 'gated', reason, reason_class: reasonClass };
294
+ return { outcome: 'skipped', reason, reason_class: reasonClass };
273
295
  }
274
296
 
275
297
  /**
@@ -287,8 +309,9 @@ function classifyFlush(res) {
287
309
  * statement) — this function deliberately does not re-implement it, so there is
288
310
  * exactly one place where "may we send?" is decided.
289
311
  *
290
- * Always emits `orchestrator.telemetry.flush` with `{ outcome, reason }` and
291
- * NOTHING else — no payload, no anon_id. Per `.claude/rules/host-resources.md`
312
+ * Always emits `orchestrator.telemetry.flush` with `{ outcome, reason,
313
+ * reason_class }` and NOTHING else — no payload, no anon_id. Per
314
+ * `.claude/rules/host-resources.md`
292
315
  * HR-105, a mechanism whose firing rate nothing records cannot be falsified;
293
316
  * this event is what makes the flush rate measurable next time.
294
317
  *
@@ -305,13 +328,17 @@ async function flushTelemetry(projectRoot) {
305
328
  }))
306
329
  // `persistence: false` means this session leaves no durable local trace;
307
330
  // a telemetry ping is a durable record too, so it honours the same switch.
308
- : { outcome: 'skipped', reason: 'persistence-disabled' };
331
+ // `reason_class` is spelled out on BOTH non-flush() branches so every
332
+ // record of this event carries the same three keys — a consumer grouping
333
+ // by class must not have to treat "absent" as a fourth class.
334
+ : { outcome: 'skipped', reason: 'persistence-disabled', reason_class: 'persistence-disabled' };
309
335
  } catch {
310
- result = { outcome: 'skipped', reason: 'error' };
336
+ result = { outcome: 'skipped', reason: 'error', reason_class: 'error' };
311
337
  }
312
338
 
313
- // `result` is passed through verbatim: it holds EXACTLY {outcome, reason}, so
314
- // no payload field can leak into the event by accident.
339
+ // `result` is passed through verbatim: it holds EXACTLY
340
+ // {outcome, reason, reason_class}, so no payload field can leak into the
341
+ // event by accident.
315
342
  try {
316
343
  await emitEvent('orchestrator.telemetry.flush', result);
317
344
  } catch { /* observability is best-effort */ }
@@ -618,8 +645,20 @@ async function main() {
618
645
  // Dead-by-age relaxation (relaxDeadByAge/assumeDeadBeforeMs, #731 — used by the
619
646
  // historical migration CLI) is NEVER passed here: a foreign lock that is live
620
647
  // at hook-time is, by definition, a real active session, not stale history.
648
+ // `ownSessionIsEnding` (#1376) rides the SAME `isRecordedSession`
649
+ // attestation as `duration_ms` / `semantic_session_id` above, for the
650
+ // same reason: it is a claim made ON BEHALF of the recorded session, and
651
+ // a foreign terminating window that inherited this repo's
652
+ // `current-session.json` identity (#863 defect (b)) must never make it.
653
+ // With the attestation false the core keeps its blanket own-live-lock
654
+ // skip, so the #863 behaviour is preserved exactly where #863 applied.
621
655
  try {
622
- const res = await backfillAbandonedSession({ repoRoot: projectRoot, sessionId, semanticSessionId });
656
+ const res = await backfillAbandonedSession({
657
+ repoRoot: projectRoot,
658
+ sessionId,
659
+ semanticSessionId,
660
+ ownSessionIsEnding: isRecordedSession,
661
+ });
623
662
  await emitBackfillOutcome('abandoned', res, { sessionId, semanticSessionId });
624
663
  } catch { /* best-effort — never block teardown */ }
625
664
 
@@ -627,7 +666,7 @@ async function main() {
627
666
  // above and to THIS session's own id: it reads STATE.md directly and
628
667
  // repairs a PAST session whose `status: completed` was set (by hand or
629
668
  // otherwise) without session-end's Phase 3.7 ever writing the matching
630
- // sessions.jsonl record — the exact state `commands/close.md`'s
669
+ // sessions.jsonl record — the exact state `skills/close/SKILL.md`'s
631
670
  // Pre-Check then reads as "already finalized" forever after. Cheap
632
671
  // no-op on the overwhelmingly common path (STATE.md status is
633
672
  // 'active'/'paused'/'idle', or the record already exists).
@@ -818,6 +857,14 @@ async function main() {
818
857
  }
819
858
 
820
859
  // Exit 0 always — informational hook must never block session teardown.
821
- main()
822
- .catch(() => {})
823
- .finally(() => process.exit(0));
860
+ // Entry guard (#1298 P7): run only as the node script every harness execs
861
+ // (`sh run-node.sh <this file>`); a bare `import()` must not tear down the
862
+ // live repo's session state.
863
+ if (isMainModule(import.meta.url)) {
864
+ // #1393: the profile gate sits INSIDE the entry guard — at module top level
865
+ // its `process.exit(0)` exited every process that merely imported this hook.
866
+ if (!shouldRunHook('on-session-end')) process.exit(0);
867
+ main()
868
+ .catch(() => {})
869
+ .finally(() => process.exit(0));
870
+ }
@@ -36,8 +36,6 @@ 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';
43
41
  import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
@@ -54,6 +52,7 @@ import { detectColdStart, consumeMarker } from '../scripts/lib/cold-start-detect
54
52
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
55
53
  import { readTelemetryState, resolveConsent, isCiEnv } from '../scripts/lib/telemetry/consent.mjs';
56
54
  import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
55
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
57
56
  // SSOT for the "is this a re-entry into the same logical session?" question
58
57
  // (#1091). Defined in the lock-bootstrap leaf module, which this hook already
59
58
  // loads, so the preservation branch below and the lock force-refresh gate can
@@ -453,8 +452,10 @@ async function resolveSessionId(input, projectRoot) {
453
452
  // observe. A naive full overwrite of current-session.json drops the
454
453
  // `last_wave` / `last_batch` markers written mid-session by
455
454
  // 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.
455
+ // re-read last_wave as absent→0 and RE-OPEN wave N mid-wave, re-stamping
456
+ // wave_start_sha and so moving the #980 files_changed start point. (Until
457
+ // 2026-09-19 it also re-emitted a duplicate wave.started{N}; that event
458
+ // has since been removed.)
458
459
  //
459
460
  // Three cases, exactly one of which preserves nothing:
460
461
  // 'raw-id' — same-logical-session source AND the recorded raw
@@ -764,8 +765,12 @@ async function main() {
764
765
  // v3.1.0 env-aware banner (opt-out via enable-host-banner: false in Session Config).
765
766
  // The ask-via-tool nudge rides the same opt-out flag — both are coordinator
766
767
  // reminders shown at session start; users who silence one expect silence.
768
+ // Hoisted because the cold-start DISPLAY below rides the same opt-out (#1133)
769
+ // and `bannerData` is not a usable proxy for it: `emitHostBanner()` returns
770
+ // null on its own error paths, which would silently re-open the nudge.
771
+ const hostBannerEnabled = await isHostBannerEnabled(projectRoot);
767
772
  let bannerData = null;
768
- if (await isHostBannerEnabled(projectRoot)) {
773
+ if (hostBannerEnabled) {
769
774
  bannerData = await emitHostBanner(projectRoot);
770
775
  // Always-on nudge: a user decision has three legitimate forms and AUQ-001
771
776
  // routes between them in order — operator verb first (nothing is blocked
@@ -795,7 +800,14 @@ async function main() {
795
800
  enabled: coldStartCfg.enabled !== false,
796
801
  });
797
802
  if (decision.shouldEmit) {
798
- pushBanner(decision.bannerLines.join('\n'));
803
+ // #1133 — the DISPLAY rides the enable-host-banner opt-out like every
804
+ // other start-of-session emitter ("users who silence one expect
805
+ // silence"). The one-shot marker consumption below does NOT: the
806
+ // opt-out silences the display, never the measurement or the state
807
+ // transition. Consuming it unconditionally is what keeps the nudge
808
+ // one-shot for an operator who has banners off — otherwise it re-arms
809
+ // on every start forever.
810
+ if (hostBannerEnabled) pushBanner(decision.bannerLines.join('\n'));
799
811
  if (decision.markerPath) {
800
812
  await consumeMarker(decision.markerPath).catch(() => false);
801
813
  }
@@ -1219,6 +1231,14 @@ async function main() {
1219
1231
  // Size-based rotation of events.jsonl (#251). Session-start is the single
1220
1232
  // rotation trigger — per-append overhead is rejected design. Any failure
1221
1233
  // is swallowed: rotation must NEVER block the hook.
1234
+ //
1235
+ // #1401 — the DURABLE record of a rotation is no longer this block. Until
1236
+ // 2026-09-19 the only evidence a rotation had happened was the `console.error`
1237
+ // below, and this hook's stderr is discarded by the harness: a rotation and a
1238
+ // DELETED archive were byte-identical from outside. `maybeRotate()` now writes
1239
+ // an `orchestrator.events.rotated` record as the first line of the new active
1240
+ // file, so the ledger carries its own break whatever happens to this stderr
1241
+ // line. The line stays as an operator convenience only — never as the record.
1222
1242
  try {
1223
1243
  let rotCfg = { enabled: true, 'max-size-mb': 10, 'max-backups': 5 };
1224
1244
  try {
@@ -1234,7 +1254,13 @@ async function main() {
1234
1254
  enabled: rotCfg.enabled !== false,
1235
1255
  });
1236
1256
  if (result.rotated) {
1237
- console.error(`events-rotation: archived ${result.archivedAs} (${result.sizeBefore} bytes)`);
1257
+ const pruned = result.pruned?.length ? `, pruned ${result.pruned.length}` : '';
1258
+ const ledger = result.recordWritten ? '' : ' [WARN: rotation record NOT written]';
1259
+ console.error(
1260
+ `events-rotation: archived ${result.archivedAs} ` +
1261
+ `(${result.sizeBefore} bytes, ${result.lines ?? '?'} lines, ` +
1262
+ `${result.firstTs ?? 'unknown'} → ${result.lastTs ?? 'unknown'}${pruned})${ledger}`,
1263
+ );
1238
1264
  }
1239
1265
  } catch (err) {
1240
1266
  console.error(`events-rotation: skipped (${err?.message ?? err})`);
@@ -1245,7 +1271,18 @@ async function main() {
1245
1271
  // flushBanner() runs here too so a throw partway through main() still surfaces
1246
1272
  // whatever was already collected; it is idempotent, so the normal path (which
1247
1273
  // flushes at the end of main) does not double-emit.
1248
- main().catch(() => {}).finally(() => {
1249
- flushBanner();
1250
- process.exit(0);
1251
- });
1274
+ //
1275
+ // Entry guard (#1298 P7): every harness execs this file as the node script
1276
+ // (`sh run-node.sh <this file>` → `exec node "$@"`), so argv[1] IS this module
1277
+ // on every real path. A bare `import()` — a probe, a test, a curious agent —
1278
+ // must not run main() against the live repo: incident W4-FX1 overwrote
1279
+ // `.orchestrator/current-session.json` exactly that way.
1280
+ if (isMainModule(import.meta.url)) {
1281
+ // #1393: the profile gate sits INSIDE the entry guard — at module top level
1282
+ // its `process.exit(0)` exited every process that merely imported this hook.
1283
+ if (!shouldRunHook('on-session-start')) process.exit(0);
1284
+ main().catch(() => {}).finally(() => {
1285
+ flushBanner();
1286
+ process.exit(0);
1287
+ });
1288
+ }