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
@@ -0,0 +1,636 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-hook-entry-guards.mjs — recurrence guard for #1393: every hook
4
+ * REGISTERED in a platform manifest must run its handler ONLY when it is the
5
+ * script node was invoked with. #1422.
6
+ *
7
+ * ## The gap this fills (and why the existing checker cannot)
8
+ *
9
+ * `./check-entry-guard.mjs` (#1371) censuses guards that are ALREADY THERE for
10
+ * symlink-fragile comparison forms. Its oracle only ever looks at statement
11
+ * fragments that mention the invocation-path expression — so a hook file
12
+ * carrying NO guard at all is never seen by it and passes trivially. That is
13
+ * exactly the regression #1393 fixed across 25 hooks, and exactly the shape
14
+ * nothing would have caught coming back.
15
+ *
16
+ * The two checkers are complements, not overlaps:
17
+ *
18
+ * | checker | question |
19
+ * |------------------------|-------------------------------------------|
20
+ * | `check-entry-guard` | is the guard that EXISTS written safely? |
21
+ * | this one | is there a guard AT ALL, around the entry? |
22
+ *
23
+ * ## Why it matters, measured (#1393, 2026-09-20)
24
+ *
25
+ * Before the sweep, under `SO_HOOK_PROFILE=off`, EVERY one of the 11
26
+ * registered hooks in one agent's half killed the process that merely
27
+ * IMPORTED it — the profile gate's `process.exit(0)` sat at module top level.
28
+ * Under a default environment only 3 of 28 were observably broken, because
29
+ * the exit is CONDITIONAL. A defect that is invisible in the common
30
+ * environment and total in the uncommon one is precisely the kind worth a
31
+ * mechanical oracle rather than a reviewer's attention.
32
+ *
33
+ * The cost side has a name too: `post-tool-failure-corrective-context.mjs`
34
+ * did not merely exit on import — it WROTE a PostToolUseFailure envelope onto
35
+ * the importer's stdout.
36
+ *
37
+ * ## Population
38
+ *
39
+ * The union of every `.mjs` path appearing in a `command` string of the four
40
+ * platform manifests: `hooks/hooks.json` (Claude Code), `hooks-codex.json`,
41
+ * `hooks-cursor.json`, `hooks-pi.json`. The three foreign manifests are
42
+ * subsets of the first today, but they are read anyway so a hook registered
43
+ * ONLY on a foreign platform can never fall out of the census. A manifest
44
+ * file that does not exist is SKIPPED (not an error): a consumer repo, or a
45
+ * fork, may legitimately ship fewer platforms.
46
+ *
47
+ * A path is normalised by taking the last `hooks` path SEGMENT onward, so
48
+ * `"$CURSOR_PLUGIN_ROOT/hooks/on-stop.mjs"` and `hooks/on-stop.mjs` are one
49
+ * entry, while a hypothetical `subhooks/x.mjs` is not mistaken for one (the
50
+ * match is on a whole segment, never a substring).
51
+ *
52
+ * ## Findings
53
+ *
54
+ * - `missing-entry-guard` — the file has no guard construct at all, or a
55
+ * module-top-level `main(...)` call sits OUTSIDE one. Either way a bare
56
+ * `import()` of the file runs the handler.
57
+ * - `toplevel-profile-exit` — a `shouldRunHook(...)` call at module top level,
58
+ * outside the guard, in a statement that also calls `process.exit`. This is
59
+ * the #1393 defect verbatim: importing the hook terminates the IMPORTER
60
+ * whenever the profile happens to disable that hook.
61
+ *
62
+ * `unregistered-file` is deliberately NOT a finding here — whether a hook file
63
+ * on disk is wired into a manifest is `check-plugin-hooks`-shaped work, a
64
+ * different question with a different population.
65
+ *
66
+ * ## Method (AST, not regex)
67
+ *
68
+ * `@babel/parser` (already a `dependencies` entry, used for the identical
69
+ * domain by `check-hooks-emit-event-guard.mjs` and
70
+ * `check-guard-requires-parity.mjs`), `sourceType: 'module'` with
71
+ * `topLevelAwait` + `importMeta`. The two questions asked — "does this call
72
+ * sit lexically inside an `if` whose test is the guard" and "is this call at
73
+ * module top level, i.e. inside no function" — are questions about lexical
74
+ * nesting, which a text scan answers only by accident: a brace inside a
75
+ * string, a template literal or a regex literal defeats brace counting, and
76
+ * #1383 is this repo's own record of a regex literal blanking a guard for a
77
+ * lexer that did not know regex literals.
78
+ *
79
+ * A file that fails to parse is a tool error (exit 2), never a silent skip.
80
+ *
81
+ * ## Recognised guard forms
82
+ *
83
+ * if (isMainModule(import.meta.url)) { … } // 24 of 27 hooks
84
+ * if (invokedAsScript()) { … } // the documented inline
85
+ * // form, 3 of 27 hooks
86
+ * const isMain = isMainModule(import.meta.url); // the indirected variant
87
+ * if (isMain) { … }
88
+ *
89
+ * NAMED CEILING (BV-004): the entry call this checker recognises is
90
+ * `main(...)` by name. A hook naming its entry differently is still covered by
91
+ * the stronger of the two rules — a file with NO guard construct at all is a
92
+ * finding regardless of how its entry is spelled — so the ceiling costs only
93
+ * the second rule (a SECOND, unguarded entry call in a file that does have a
94
+ * guard). REVISIT by extending `ENTRY_NAMES` if a hook ever names its entry
95
+ * something else; do not weaken the rule to "any top-level call", which would
96
+ * flag every legitimate top-level `const X = loadThing()`.
97
+ *
98
+ * ## Mode: BLOCKING
99
+ *
100
+ * Census at HEAD 98d06598 (2026-09-22): 27 registered `.mjs`, 27 guarded,
101
+ * 0 top-level profile exits. A blocking gate is only dishonest when it is red
102
+ * on arrival for work nobody intends to do; this one is green on arrival
103
+ * because #1393 drained the backlog first.
104
+ *
105
+ * Usage: check-hook-entry-guards.mjs [<repo-root>]
106
+ * Output: ` PASS: …` / ` FAIL: …` lines (two leading spaces), then
107
+ * `Results: N passed, M failed`. Exit 0 = clean, 1 = finding(s), 2 = tool
108
+ * error (unparseable/unreadable hook, or an empty census).
109
+ *
110
+ * Import-safety: importing this module MUST NOT execute anything — the
111
+ * isMain guard at the bottom is the only side-effecting path. (This checker
112
+ * would report itself otherwise.)
113
+ */
114
+
115
+ import { existsSync, readFileSync } from 'node:fs';
116
+ import path from 'node:path';
117
+ import { parse } from '@babel/parser';
118
+ import { isMainModule } from '../is-main-module.mjs';
119
+
120
+ /**
121
+ * Exempted hook files, `Map<repo-relative-path, reason>`.
122
+ *
123
+ * EMPTY on arrival and kept as a frozen MECHANISM rather than deleted — the
124
+ * same shape `check-entry-guard.mjs`'s `ALLOWLIST` and
125
+ * `check-hooks-emit-event-guard.mjs`'s `BASELINE_UNGUARDED` already use in
126
+ * this directory: the next hand-verified exception is exactly what would
127
+ * re-populate it, and an entry that carries no reason from day one rots into
128
+ * a silent permanent suppression. A baselined file still REPORTS (as a WARN),
129
+ * and an entry that no longer matches any finding is itself a FAIL
130
+ * (`baseline-stale`), so the list drains itself.
131
+ *
132
+ * @type {Map<string, string>}
133
+ */
134
+ export const BASELINE = Object.freeze(new Map());
135
+
136
+ /** The four platform manifests, repo-relative. A missing one is skipped. */
137
+ export const MANIFESTS = Object.freeze([
138
+ 'hooks/hooks.json',
139
+ 'hooks/hooks-codex.json',
140
+ 'hooks/hooks-cursor.json',
141
+ 'hooks/hooks-pi.json',
142
+ ]);
143
+
144
+ /** Entry-function names treated as "runs the handler". See the NAMED CEILING. */
145
+ const ENTRY_NAMES = new Set(['main']);
146
+
147
+ /** Node types that open a new function scope — crossing one leaves top level. */
148
+ const FUNCTION_TYPES = new Set([
149
+ 'FunctionDeclaration',
150
+ 'FunctionExpression',
151
+ 'ArrowFunctionExpression',
152
+ 'ObjectMethod',
153
+ 'ClassMethod',
154
+ 'ClassPrivateMethod',
155
+ ]);
156
+
157
+ /** AST metadata keys with no code nesting — never descended into. */
158
+ const SKIP_KEYS = new Set([
159
+ 'loc', 'start', 'end', 'extra', 'tokens', 'comments', 'errors',
160
+ 'leadingComments', 'trailingComments', 'innerComments',
161
+ ]);
162
+
163
+ /**
164
+ * The exact shape a registered hook's tail must have. Quoted verbatim in
165
+ * every remedy so the fix needs no second file open.
166
+ *
167
+ * @param {string} hookName basename without extension
168
+ * @returns {string}
169
+ */
170
+ function targetForm(hookName) {
171
+ return (
172
+ `if (isMainModule(import.meta.url)) { ` +
173
+ `if (!shouldRunHook('${hookName}')) process.exit(0); main()... }`
174
+ );
175
+ }
176
+
177
+ const WHY =
178
+ '#1393: a bare `import()` of a registered hook must run no handler and must ' +
179
+ 'not terminate the importing process — measured under SO_HOOK_PROFILE=off, ' +
180
+ 'every hook whose profile gate sat at module top level killed its importer, ' +
181
+ "and one of them wrote a PostToolUseFailure envelope onto the importer's stdout";
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Population
185
+ // ---------------------------------------------------------------------------
186
+
187
+ /**
188
+ * Every `command` string value anywhere inside a parsed manifest.
189
+ *
190
+ * @param {unknown} node
191
+ * @param {string[]} out
192
+ * @returns {void}
193
+ */
194
+ function collectCommands(node, out) {
195
+ if (Array.isArray(node)) {
196
+ for (const item of node) collectCommands(item, out);
197
+ return;
198
+ }
199
+ if (!node || typeof node !== 'object') return;
200
+ for (const [key, value] of Object.entries(node)) {
201
+ if (key === 'command' && typeof value === 'string') out.push(value);
202
+ else collectCommands(value, out);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Normalise one whitespace/quote-delimited token to a repo-relative hook path,
208
+ * anchored on a whole `hooks` path SEGMENT (so `subhooks/x.mjs` is not one).
209
+ *
210
+ * @param {string} token
211
+ * @returns {string | null}
212
+ */
213
+ function normaliseHookPath(token) {
214
+ const segments = token.split('/');
215
+ const idx = segments.lastIndexOf('hooks');
216
+ if (idx === -1) return null;
217
+ return segments.slice(idx).join('/');
218
+ }
219
+
220
+ const MJS_TOKEN = /[^\s"']*\/?hooks\/[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*\.mjs/g;
221
+
222
+ /**
223
+ * The union population: every `.mjs` registered by any present manifest.
224
+ *
225
+ * @param {string} repoRoot absolute repo root
226
+ * @returns {{files: string[], manifests: {rel: string, present: boolean, count: number}[], errors: {file: string, message: string}[]}}
227
+ */
228
+ export function collectRegisteredHooks(repoRoot) {
229
+ /** @type {Map<string, Set<string>>} hook path -> manifests registering it */
230
+ const registry = new Map();
231
+ const manifests = [];
232
+ const errors = [];
233
+
234
+ for (const rel of MANIFESTS) {
235
+ const abs = path.join(repoRoot, rel);
236
+ if (!existsSync(abs)) {
237
+ manifests.push({ rel, present: false, count: 0 });
238
+ continue;
239
+ }
240
+ let parsed;
241
+ try {
242
+ parsed = JSON.parse(readFileSync(abs, 'utf8'));
243
+ } catch (err) {
244
+ errors.push({ file: rel, message: `unparseable manifest: ${err?.message ?? String(err)}` });
245
+ manifests.push({ rel, present: true, count: 0 });
246
+ continue;
247
+ }
248
+ /** @type {string[]} */
249
+ const commands = [];
250
+ collectCommands(parsed, commands);
251
+ let count = 0;
252
+ for (const command of commands) {
253
+ MJS_TOKEN.lastIndex = 0;
254
+ let match;
255
+ while ((match = MJS_TOKEN.exec(command)) !== null) {
256
+ const hookPath = normaliseHookPath(match[0]);
257
+ if (!hookPath) continue;
258
+ if (!registry.has(hookPath)) registry.set(hookPath, new Set());
259
+ registry.get(hookPath).add(rel);
260
+ count += 1;
261
+ }
262
+ }
263
+ manifests.push({ rel, present: true, count });
264
+ }
265
+
266
+ return { files: [...registry.keys()].sort(), manifests, errors };
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // AST oracle
271
+ // ---------------------------------------------------------------------------
272
+
273
+ /**
274
+ * @param {string} source
275
+ * @param {string} filename repo-relative, for parse-error messages only
276
+ * @returns {import('@babel/parser').ParseResult<import('@babel/types').File>}
277
+ */
278
+ function parseModule(source, filename) {
279
+ return parse(source, {
280
+ sourceType: 'module',
281
+ sourceFilename: filename,
282
+ errorRecovery: false,
283
+ plugins: ['topLevelAwait', 'importMeta'],
284
+ });
285
+ }
286
+
287
+ /**
288
+ * `isMainModule(import.meta.url)` or `invokedAsScript()`.
289
+ *
290
+ * @param {any} node
291
+ * @returns {boolean}
292
+ */
293
+ function isGuardCall(node) {
294
+ if (!node || node.type !== 'CallExpression' || node.callee?.type !== 'Identifier') return false;
295
+ const name = node.callee.name;
296
+ if (name === 'invokedAsScript') return node.arguments.length === 0;
297
+ if (name !== 'isMainModule') return false;
298
+ const arg = node.arguments[0];
299
+ return (
300
+ arg?.type === 'MemberExpression' &&
301
+ arg.property?.type === 'Identifier' &&
302
+ arg.property.name === 'url' &&
303
+ arg.object?.type === 'MetaProperty'
304
+ );
305
+ }
306
+
307
+ /**
308
+ * Module-level `const X = <guard call>` names — the indirected guard form
309
+ * (`const isMain = invokedAsScript(); if (isMain) {…}`), used by
310
+ * `hooks/post-bash-write-verify.mjs` among others.
311
+ *
312
+ * @param {any} program
313
+ * @returns {Set<string>}
314
+ */
315
+ function guardVariableNames(program) {
316
+ const names = new Set();
317
+ for (const stmt of program.body) {
318
+ if (stmt.type !== 'VariableDeclaration') continue;
319
+ for (const decl of stmt.declarations) {
320
+ if (decl.id?.type === 'Identifier' && isGuardCall(decl.init)) names.add(decl.id.name);
321
+ }
322
+ }
323
+ return names;
324
+ }
325
+
326
+ /**
327
+ * Is this `if` test the entry guard? Accepts the direct call, the indirected
328
+ * identifier, and either side of a `&&`/`||` (a guard ANDed with an extra
329
+ * condition still gates the body behind the guard).
330
+ *
331
+ * @param {any} node
332
+ * @param {Set<string>} guardVars
333
+ * @returns {boolean}
334
+ */
335
+ function isGuardTest(node, guardVars) {
336
+ if (!node) return false;
337
+ if (isGuardCall(node)) return true;
338
+ if (node.type === 'Identifier') return guardVars.has(node.name);
339
+ if (node.type === 'LogicalExpression') {
340
+ return isGuardTest(node.left, guardVars) || isGuardTest(node.right, guardVars);
341
+ }
342
+ return false;
343
+ }
344
+
345
+ /**
346
+ * Walk the ancestor chain outward.
347
+ *
348
+ * @param {{node: any, key: string | number}[]} ancestors outer→inner
349
+ * @param {Set<string>} guardVars
350
+ * @returns {{inFunction: boolean, inGuard: boolean}}
351
+ */
352
+ function classifyPosition(ancestors, guardVars) {
353
+ let inFunction = false;
354
+ let inGuard = false;
355
+ for (let i = ancestors.length - 1; i >= 0; i -= 1) {
356
+ const { node, key } = ancestors[i];
357
+ if (FUNCTION_TYPES.has(node.type)) inFunction = true;
358
+ if (
359
+ node.type === 'IfStatement' &&
360
+ key === 'consequent' &&
361
+ isGuardTest(node.test, guardVars)
362
+ ) {
363
+ inGuard = true;
364
+ }
365
+ }
366
+ return { inFunction, inGuard };
367
+ }
368
+
369
+ /**
370
+ * @typedef {{kind: 'missing-entry-guard' | 'toplevel-profile-exit', file: string, line: number, detail: string}} Finding
371
+ */
372
+
373
+ /**
374
+ * Analyse one hook module body.
375
+ *
376
+ * @param {string} source
377
+ * @param {string} rel repo-relative path (finding key + parse-error message)
378
+ * @returns {{findings: Finding[], guardCount: number, entryCalls: number}}
379
+ */
380
+ export function analyzeHookSource(source, rel) {
381
+ const ast = parseModule(source, rel);
382
+ const program = ast.program;
383
+ const guardVars = guardVariableNames(program);
384
+
385
+ let guardCount = 0;
386
+ let entryCalls = 0;
387
+ /** @type {Finding[]} */
388
+ const findings = [];
389
+ const hookName = path.basename(rel, '.mjs');
390
+
391
+ /** Top-level statement index → does it contain a top-level `process.exit` call? */
392
+ const exitStatements = new Set();
393
+ /** Candidate top-level unguarded `shouldRunHook` calls, keyed by statement index. */
394
+ const profileCalls = [];
395
+
396
+ let stmtIndex = -1;
397
+
398
+ /**
399
+ * @param {any} node
400
+ * @param {{node: any, key: string | number}[]} ancestors
401
+ */
402
+ function visit(node, ancestors) {
403
+ if (Array.isArray(node)) {
404
+ for (const item of node) visit(item, ancestors);
405
+ return;
406
+ }
407
+ if (!node || typeof node !== 'object' || typeof node.type !== 'string') return;
408
+
409
+ if (node.type === 'IfStatement' && isGuardTest(node.test, guardVars)) guardCount += 1;
410
+
411
+ if (node.type === 'CallExpression') {
412
+ const callee = node.callee;
413
+ const calleeName = callee?.type === 'Identifier' ? callee.name : null;
414
+
415
+ if (calleeName && ENTRY_NAMES.has(calleeName)) {
416
+ const { inFunction, inGuard } = classifyPosition(ancestors, guardVars);
417
+ if (!inFunction) {
418
+ entryCalls += 1;
419
+ if (!inGuard) {
420
+ findings.push({
421
+ kind: 'missing-entry-guard',
422
+ file: rel,
423
+ line: node.loc?.start?.line ?? 0,
424
+ detail:
425
+ `module-top-level \`${calleeName}()\` call outside any entry guard — ` +
426
+ `importing this file RUNS the handler. Wrap it: ${targetForm(hookName)}`,
427
+ });
428
+ }
429
+ }
430
+ }
431
+
432
+ if (calleeName === 'shouldRunHook') {
433
+ const { inFunction, inGuard } = classifyPosition(ancestors, guardVars);
434
+ if (!inFunction && !inGuard) {
435
+ profileCalls.push({ stmtIndex, line: node.loc?.start?.line ?? 0 });
436
+ }
437
+ }
438
+
439
+ if (
440
+ callee?.type === 'MemberExpression' &&
441
+ callee.object?.type === 'Identifier' &&
442
+ callee.object.name === 'process' &&
443
+ callee.property?.type === 'Identifier' &&
444
+ callee.property.name === 'exit'
445
+ ) {
446
+ const { inFunction } = classifyPosition(ancestors, guardVars);
447
+ if (!inFunction) exitStatements.add(stmtIndex);
448
+ }
449
+ }
450
+
451
+ for (const [key, child] of Object.entries(node)) {
452
+ if (SKIP_KEYS.has(key)) continue;
453
+ if (child && typeof child === 'object') {
454
+ ancestors.push({ node, key });
455
+ visit(child, ancestors);
456
+ ancestors.pop();
457
+ }
458
+ }
459
+ }
460
+
461
+ for (let i = 0; i < program.body.length; i += 1) {
462
+ stmtIndex = i;
463
+ visit(program.body[i], [{ node: program, key: 'body' }]);
464
+ }
465
+
466
+ for (const call of profileCalls) {
467
+ if (!exitStatements.has(call.stmtIndex)) continue;
468
+ findings.push({
469
+ kind: 'toplevel-profile-exit',
470
+ file: rel,
471
+ line: call.line,
472
+ detail:
473
+ 'module-top-level `shouldRunHook(…)` whose statement calls `process.exit` — ' +
474
+ 'importing this file TERMINATES the importing process whenever the profile ' +
475
+ `disables this hook. Move the gate inside the entry guard: ${targetForm(hookName)}`,
476
+ });
477
+ }
478
+
479
+ if (guardCount === 0) {
480
+ findings.unshift({
481
+ kind: 'missing-entry-guard',
482
+ file: rel,
483
+ line: 0,
484
+ detail:
485
+ 'no entry guard anywhere in the file — neither `if (isMainModule(import.meta.url))` ' +
486
+ `nor \`if (invokedAsScript())\`. Required tail: ${targetForm(hookName)}`,
487
+ });
488
+ }
489
+
490
+ return { findings, guardCount, entryCalls };
491
+ }
492
+
493
+ /**
494
+ * Scan every registered hook.
495
+ *
496
+ * @param {string} repoRoot absolute repo root
497
+ * @param {{baseline?: Map<string, string>}} [options] `baseline` defaults to
498
+ * the module-level {@link BASELINE} (empty in production); injectable so
499
+ * tests exercise the WARN / `baseline-stale` paths without mutating it.
500
+ * @returns {{findings: Finding[], baselineFindings: {key: string, message: string}[], toolErrors: {file: string, message: string}[], registered: string[], guarded: number, manifests: {rel: string, present: boolean, count: number}[]}}
501
+ */
502
+ export function scanHookEntryGuards(repoRoot, { baseline = BASELINE } = {}) {
503
+ const { files, manifests, errors } = collectRegisteredHooks(repoRoot);
504
+ /** @type {Finding[]} */
505
+ const findings = [];
506
+ /** @type {{file: string, message: string}[]} */
507
+ const toolErrors = [...errors];
508
+ const flagged = new Set();
509
+ let guarded = 0;
510
+
511
+ for (const rel of files) {
512
+ const abs = path.join(repoRoot, rel);
513
+ let source;
514
+ try {
515
+ source = readFileSync(abs, 'utf8');
516
+ } catch (err) {
517
+ toolErrors.push({ file: rel, message: `registered but unreadable: ${err?.message ?? String(err)}` });
518
+ continue;
519
+ }
520
+ let result;
521
+ try {
522
+ result = analyzeHookSource(source, rel);
523
+ } catch (err) {
524
+ toolErrors.push({ file: rel, message: `parse failed: ${err?.message ?? String(err)}` });
525
+ continue;
526
+ }
527
+ if (result.findings.length === 0) guarded += 1;
528
+ else flagged.add(rel);
529
+ for (const finding of result.findings) {
530
+ findings.push({ ...finding, baselined: baseline.has(rel) });
531
+ }
532
+ }
533
+
534
+ /** @type {{key: string, message: string}[]} */
535
+ const baselineFindings = [];
536
+ for (const [key, reason] of baseline.entries()) {
537
+ if (!flagged.has(key)) {
538
+ baselineFindings.push({
539
+ key,
540
+ message: 'baseline entry no longer matches a finding (guarded or deregistered) — remove it',
541
+ });
542
+ } else if (String(reason ?? '').trim() === '') {
543
+ baselineFindings.push({
544
+ key,
545
+ message: 'baseline entry has no reason — name the linked issue or remove the entry',
546
+ });
547
+ }
548
+ }
549
+
550
+ return { findings, baselineFindings, toolErrors, registered: files, guarded, manifests };
551
+ }
552
+
553
+ // ---------------------------------------------------------------------------
554
+ // CLI
555
+ // ---------------------------------------------------------------------------
556
+
557
+ /**
558
+ * Run the check, printing the validate-plugin line vocabulary.
559
+ *
560
+ * @param {string} repoRoot absolute repo root
561
+ * @param {{baseline?: Map<string, string>}} [options] forwarded to {@link scanHookEntryGuards}
562
+ * @returns {number} 0 clean · 1 finding(s) · 2 tool error (incl. empty census)
563
+ */
564
+ export function runCheckHookEntryGuards(repoRoot, options) {
565
+ const { findings, baselineFindings, toolErrors, registered, guarded, manifests } =
566
+ scanHookEntryGuards(repoRoot, options);
567
+
568
+ if (toolErrors.length > 0) {
569
+ for (const e of toolErrors) console.log(` FAIL: ${e.file} — ${e.message}`);
570
+ console.log('');
571
+ console.log(`Results: 0 passed, ${toolErrors.length} failed`);
572
+ return 2;
573
+ }
574
+
575
+ // Vacuum guard: an empty census is never green. A manifest that moved, a
576
+ // command shape this extractor stopped recognising, or a fixture root with
577
+ // no hooks at all would otherwise report PASS over nothing — the exact
578
+ // failure mode this checker exists to prevent, one level up.
579
+ if (registered.length === 0) {
580
+ const present = manifests.filter((m) => m.present).map((m) => m.rel);
581
+ console.log(
582
+ ` FAIL: no registered hook .mjs found in ${present.length} present manifest(s)` +
583
+ `${present.length > 0 ? ` (${present.join(', ')})` : ''} — an empty census cannot be clean`,
584
+ );
585
+ console.log('');
586
+ console.log('Results: 0 passed, 1 failed');
587
+ return 2;
588
+ }
589
+
590
+ const live = findings.filter((f) => !f.baselined);
591
+ const warned = findings.filter((f) => f.baselined);
592
+
593
+ if (live.length === 0 && baselineFindings.length === 0) {
594
+ const presentCount = manifests.filter((m) => m.present).length;
595
+ console.log(
596
+ ` PASS: ${guarded}/${registered.length} registered hook module(s) across ` +
597
+ `${presentCount} manifest(s) run their entry only under an entry guard ` +
598
+ '(0 missing-entry-guard, 0 toplevel-profile-exit)',
599
+ );
600
+ }
601
+ for (const f of warned) {
602
+ console.log(` WARN: ${f.file}:${f.line} — ${f.kind}: ${f.detail} (baselined)`);
603
+ }
604
+ for (const f of live) {
605
+ console.log(` FAIL: ${f.file}:${f.line} — ${f.kind}: ${f.detail}. WHY — ${WHY}`);
606
+ }
607
+ for (const b of baselineFindings) {
608
+ console.log(` FAIL: ${b.key} — ${b.message}`);
609
+ }
610
+
611
+ const failCount = live.length + baselineFindings.length;
612
+ console.log('');
613
+ console.log(`Results: ${failCount === 0 ? 1 : 0} passed, ${failCount} failed`);
614
+ return failCount > 0 ? 1 : 0;
615
+ }
616
+
617
+ if (isMainModule(import.meta.url)) {
618
+ const argv = process.argv.slice(2);
619
+ const usage =
620
+ 'Usage: check-hook-entry-guards.mjs [<repo-root>]\n' +
621
+ 'Exit: 0 clean · 1 missing entry guard / top-level profile exit · 2 tool error';
622
+ if (argv.includes('--help')) {
623
+ console.log(usage);
624
+ process.exitCode = 0;
625
+ } else {
626
+ const unknown = argv.filter((a) => a.startsWith('--') && a !== '--help');
627
+ if (unknown.length > 0) {
628
+ console.error(`Unknown flag(s): ${unknown.join(', ')}\n${usage}`);
629
+ process.exitCode = 1;
630
+ } else {
631
+ const positional = argv.filter((a) => !a.startsWith('--'));
632
+ console.log('--- Check: registered hook entry guards (#1422) ---');
633
+ process.exitCode = runCheckHookEntryGuards(path.resolve(positional[0] ?? process.cwd()));
634
+ }
635
+ }
636
+ }
@@ -79,9 +79,9 @@
79
79
 
80
80
  import { readFileSync } from 'node:fs';
81
81
  import path from 'node:path';
82
- import { pathToFileURL } from 'node:url';
83
82
  import { parse } from '@babel/parser';
84
83
  import { listRepoFiles } from './repo-files.mjs';
84
+ import { isMainModule } from '../is-main-module.mjs';
85
85
 
86
86
  // ---------------------------------------------------------------------------
87
87
  // Baseline — Map<key, reason>, keyed on
@@ -359,7 +359,7 @@ export function runCheckHooksEmitEventGuard(repoRoot, options) {
359
359
  return failCount > 0 ? 1 : 0;
360
360
  }
361
361
 
362
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
362
+ const isMain =isMainModule(import.meta.url);
363
363
  if (isMain) {
364
364
  const root = process.argv[2];
365
365
  if (!root) {
@@ -126,7 +126,6 @@
126
126
 
127
127
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
128
128
  import path from 'node:path';
129
- import { pathToFileURL } from 'node:url';
130
129
  import { readLearnings } from '../learnings/io.mjs';
131
130
  // THE shared key derivation. This check resolves a rule's STAMPED
132
131
  // `learning-key` against keys it derives from the store, so it is the one place
@@ -134,6 +133,7 @@ import { readLearnings } from '../learnings/io.mjs';
134
133
  // finding — it must derive the key with the emitter's function, not a copy of
135
134
  // the emitter's formula.
136
135
  import { learningKeyOf } from '../learnings/kebab.mjs';
136
+ import { isMainModule } from '../is-main-module.mjs';
137
137
 
138
138
  /** Directory holding the rule corpus, relative to the plugin root. */
139
139
  const RULES_REL = path.join('.claude', 'rules');
@@ -475,7 +475,7 @@ export async function runCheckLearningProvenance(pluginRoot) {
475
475
  return 0;
476
476
  }
477
477
 
478
- const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
478
+ const isMain =isMainModule(import.meta.url);
479
479
  if (isMain) {
480
480
  const argv = process.argv.slice(2);
481
481
  const flags = new Set(argv.filter((a) => a.startsWith('--')));
@@ -35,6 +35,7 @@ if (!existsSync(generator)) {
35
35
  } else {
36
36
  const detail = ((result.stdout ?? '') + (result.stderr ?? '')).trim();
37
37
  fail(`pi prompt wrappers are stale${detail ? `: ${detail}` : ''}`);
38
+ console.log(' Remedy: node scripts/generate-pi-prompts.mjs');
38
39
  }
39
40
  }
40
41