session-orchestrator 3.24.0 → 4.0.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 (350) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1125 -2
  81. package/NOTICE +11 -6
  82. package/README.md +127 -94
  83. package/agents/eval-judge.md +1 -1
  84. package/agents/skill-applied-judge.md +1 -1
  85. package/assets/wave-lifecycle.svg +98 -0
  86. package/commands/release.md +6 -3
  87. package/commands/session.md +18 -3
  88. package/docs/README.md +4 -0
  89. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  90. package/docs/baseline.md +67 -0
  91. package/docs/ci-setup.md +108 -62
  92. package/docs/codex-setup.md +65 -21
  93. package/docs/components.md +36 -15
  94. package/docs/cursor-setup.md +6 -2
  95. package/docs/events-schema.md +9 -6
  96. package/docs/instruction-delivery.md +62 -0
  97. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  98. package/docs/migration-v4.md +341 -0
  99. package/docs/pi-setup.md +6 -1
  100. package/docs/plugin-architecture-v3.md +1 -1
  101. package/docs/rule-authoring.md +85 -19
  102. package/docs/scope-collision-guard.md +5 -5
  103. package/docs/session-config-reference.md +57 -56
  104. package/docs/session-config-template.md +6 -29
  105. package/docs/telemetry.md +157 -3
  106. package/docs/vault-docs-architecture.md +50 -11
  107. package/hooks/_lib/hook-import-set.json +1487 -0
  108. package/hooks/_lib/subagent-transcript.mjs +562 -0
  109. package/hooks/config-protection.mjs +2 -2
  110. package/hooks/cwd-change-restore.mjs +2 -2
  111. package/hooks/enforce-commands.mjs +69 -0
  112. package/hooks/hooks-codex.json +1 -1
  113. package/hooks/hooks-cursor.json +10 -0
  114. package/hooks/hooks-pi.json +5 -0
  115. package/hooks/hooks.json +6 -1
  116. package/hooks/loop-guard.mjs +3 -3
  117. package/hooks/on-session-end.mjs +2 -2
  118. package/hooks/on-session-start.mjs +103 -2
  119. package/hooks/on-stop.mjs +36 -11
  120. package/hooks/operator-steer.mjs +2 -2
  121. package/hooks/post-bash-write-verify.mjs +85 -0
  122. package/hooks/post-edit-import-probe.mjs +344 -0
  123. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  124. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  125. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  126. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  127. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  128. package/hooks/skill-invocation-telemetry.mjs +17 -5
  129. package/hooks/subagent-telemetry.mjs +13 -4
  130. package/monitors/monitors.json +3 -3
  131. package/package.json +9 -1
  132. package/pi/prompts/session.md +2 -2
  133. package/plugin.json +27 -0
  134. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  135. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  136. package/scripts/dialectic-deriver.mjs +73 -8
  137. package/scripts/export-hw-learnings.mjs +113 -1
  138. package/scripts/generate-agents-skills.mjs +378 -0
  139. package/scripts/generate-cursor-adapter.mjs +45 -8
  140. package/scripts/generate-hook-import-set.mjs +249 -0
  141. package/scripts/lib/agent-status.mjs +13 -2
  142. package/scripts/lib/auto-dream.mjs +38 -36
  143. package/scripts/lib/autonomy/suitability.mjs +6 -0
  144. package/scripts/lib/autopilot/loop.mjs +2 -2
  145. package/scripts/lib/ci-status-banner.mjs +220 -75
  146. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  147. package/scripts/lib/config/auto-dream.mjs +2 -1
  148. package/scripts/lib/config/block-header.mjs +8 -0
  149. package/scripts/lib/config/block-preprocess.mjs +177 -0
  150. package/scripts/lib/config/broken-window.mjs +2 -1
  151. package/scripts/lib/config/cold-start.mjs +2 -1
  152. package/scripts/lib/config/config-protection.mjs +22 -2
  153. package/scripts/lib/config/context-coverage.mjs +2 -1
  154. package/scripts/lib/config/cross-repo.mjs +2 -1
  155. package/scripts/lib/config/custom-phases.mjs +2 -1
  156. package/scripts/lib/config/dialectic.mjs +2 -1
  157. package/scripts/lib/config/discovery-validator.mjs +2 -1
  158. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  159. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  160. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  161. package/scripts/lib/config/docs-staleness.mjs +2 -1
  162. package/scripts/lib/config/drift-check.mjs +2 -1
  163. package/scripts/lib/config/eval.mjs +2 -1
  164. package/scripts/lib/config/events-rotation.mjs +2 -1
  165. package/scripts/lib/config/evolve.mjs +8 -2
  166. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  167. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  168. package/scripts/lib/config/handover-gate.mjs +2 -1
  169. package/scripts/lib/config/health-endpoints.mjs +7 -2
  170. package/scripts/lib/config/issue-budget.mjs +2 -1
  171. package/scripts/lib/config/loop-guard.mjs +2 -1
  172. package/scripts/lib/config/memory.mjs +2 -1
  173. package/scripts/lib/config/moc-staleness.mjs +2 -1
  174. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  175. package/scripts/lib/config/private-config-dir.mjs +67 -0
  176. package/scripts/lib/config/reconcile.mjs +2 -1
  177. package/scripts/lib/config/remote-hosts.mjs +2 -1
  178. package/scripts/lib/config/section-extractor.mjs +7 -1
  179. package/scripts/lib/config/skill-evolution.mjs +2 -1
  180. package/scripts/lib/config/slopcheck.mjs +2 -1
  181. package/scripts/lib/config/state-md-lock.mjs +2 -1
  182. package/scripts/lib/config/templates-first.mjs +2 -1
  183. package/scripts/lib/config/test.mjs +2 -1
  184. package/scripts/lib/config/vault-integration.mjs +7 -1
  185. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  186. package/scripts/lib/config/vault-staleness.mjs +2 -1
  187. package/scripts/lib/config/vault-sync.mjs +2 -1
  188. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  189. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  190. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  191. package/scripts/lib/convergence-monitor.mjs +82 -16
  192. package/scripts/lib/dispatcher/rank.mjs +124 -48
  193. package/scripts/lib/ecosystem-health.mjs +16 -2
  194. package/scripts/lib/eval/engine.mjs +9 -1
  195. package/scripts/lib/eval/session-resolve.mjs +23 -4
  196. package/scripts/lib/events.mjs +22 -6
  197. package/scripts/lib/frontmatter-guard.mjs +131 -13
  198. package/scripts/lib/gates/gate-full.mjs +26 -0
  199. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  200. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  201. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  202. package/scripts/lib/host-identity.mjs +50 -11
  203. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  204. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  205. package/scripts/lib/learnings/io.mjs +60 -6
  206. package/scripts/lib/memory-proposals/store.mjs +30 -22
  207. package/scripts/lib/owner-config-banner.mjs +43 -6
  208. package/scripts/lib/owner-config-loader.mjs +21 -10
  209. package/scripts/lib/owner-interview.mjs +3 -3
  210. package/scripts/lib/owner-yaml.mjs +207 -14
  211. package/scripts/lib/platform.mjs +108 -15
  212. package/scripts/lib/plugin-update-banner.mjs +406 -0
  213. package/scripts/lib/project-hygiene.mjs +38 -2
  214. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  215. package/scripts/lib/quality-gate.mjs +133 -44
  216. package/scripts/lib/reconcile/emitter.mjs +68 -6
  217. package/scripts/lib/reconcile/engine.mjs +13 -4
  218. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  219. package/scripts/lib/reconcile/writer.mjs +40 -18
  220. package/scripts/lib/session-close-backfill.mjs +67 -9
  221. package/scripts/lib/session-id.mjs +12 -23
  222. package/scripts/lib/session-identity/own-session.mjs +125 -10
  223. package/scripts/lib/session-lock-shape.mjs +43 -0
  224. package/scripts/lib/session-lock.mjs +5 -10
  225. package/scripts/lib/session-registry.mjs +25 -9
  226. package/scripts/lib/session-schema/constants.mjs +36 -2
  227. package/scripts/lib/session-schema/validator.mjs +38 -4
  228. package/scripts/lib/session-start-probes.mjs +18 -1
  229. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  230. package/scripts/lib/skill-health/join.mjs +17 -4
  231. package/scripts/lib/state-md.mjs +78 -0
  232. package/scripts/lib/sunset/walker.mjs +6 -0
  233. package/scripts/lib/telemetry/schema.mjs +181 -9
  234. package/scripts/lib/telemetry/sync.mjs +368 -12
  235. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  236. package/scripts/lib/validate/check-agents.mjs +3 -3
  237. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  238. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  239. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  240. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  241. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  242. package/scripts/lib/validate/check-unwired-features.mjs +0 -2
  243. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  244. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  245. package/scripts/lib/vault-backfill/template.mjs +63 -6
  246. package/scripts/lib/vault-mirror/process.mjs +165 -42
  247. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  248. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  249. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  250. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  251. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  252. package/scripts/lib/wave-resource-gate.mjs +8 -2
  253. package/scripts/lib/wave-sizing.mjs +4 -1
  254. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  255. package/scripts/materialize-wave-scope.mjs +12 -5
  256. package/scripts/memory-propose.mjs +19 -5
  257. package/scripts/migrate-cold-start-seed.mjs +4 -1
  258. package/scripts/parse-config.mjs +60 -3
  259. package/scripts/release.mjs +337 -29
  260. package/scripts/repair-invalid-sessions.mjs +3 -3
  261. package/scripts/run-quality-gate.mjs +128 -11
  262. package/scripts/sweep-expired-learnings.mjs +90 -0
  263. package/scripts/sync-vault-schema.mjs +3 -1
  264. package/scripts/telemetry.mjs +2 -2
  265. package/scripts/validate-plugin.mjs +161 -0
  266. package/scripts/validate-wave-scope.mjs +28 -8
  267. package/scripts/wave-scope-binding.mjs +215 -0
  268. package/skills/_shared/instruction-file-resolution.md +10 -0
  269. package/skills/_shared/parallel-aware-preamble.md +1 -0
  270. package/skills/_shared/platform-tools.md +1 -1
  271. package/skills/_shared/state-ownership.md +1 -1
  272. package/skills/architecture/SKILL.md +7 -5
  273. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  274. package/skills/autopilot/SKILL.md +4 -18
  275. package/skills/claude-md-drift-check/SKILL.md +5 -1
  276. package/skills/claude-md-drift-check/checker.mjs +62 -2
  277. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  278. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  279. package/skills/discovery/probes-arch.md +20 -18
  280. package/skills/dispatcher/SKILL.md +3 -2
  281. package/skills/evolve/SKILL.md +65 -26
  282. package/skills/frontmatter-guard/SKILL.md +11 -5
  283. package/skills/npm-publish/SKILL.md +1 -1
  284. package/skills/reconcile/SKILL.md +33 -0
  285. package/skills/remote-offload/SKILL.md +1 -1
  286. package/skills/session-end/SKILL.md +18 -905
  287. package/skills/session-end/phase-3-6-tail.md +10 -3
  288. package/skills/session-end/plan-verification.md +221 -155
  289. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  290. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  291. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  292. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  293. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  294. package/skills/session-end/references/session-summary-template.md +62 -0
  295. package/skills/session-plan/SKILL.md +49 -0
  296. package/skills/session-start/SKILL.md +22 -904
  297. package/skills/session-start/phase-8-5-express-path.md +1 -1
  298. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  299. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  300. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  301. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  302. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  303. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  304. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  305. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  306. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  307. package/skills/vault-sync/validator.mjs +21 -27
  308. package/skills/wave-executor/SKILL.md +15 -1
  309. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  310. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  311. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  312. package/skills/wave-executor/wave-loop.md +14 -1309
  313. package/templates/_shared/journey-manifest.md +10 -6
  314. package/.cursor/commands/autopilot-multi.md +0 -14
  315. package/.cursor/commands/contract-version-bump.md +0 -14
  316. package/.cursor/commands/journey-audit.md +0 -14
  317. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  318. package/.cursor/skills/daily/SKILL.md +0 -12
  319. package/.cursor/skills/domain-model/SKILL.md +0 -13
  320. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  321. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  322. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  323. package/commands/autopilot-multi.md +0 -74
  324. package/commands/contract-version-bump.md +0 -28
  325. package/commands/journey-audit.md +0 -43
  326. package/pi/prompts/autopilot-multi.md +0 -12
  327. package/pi/prompts/contract-version-bump.md +0 -12
  328. package/pi/prompts/journey-audit.md +0 -12
  329. package/scripts/autopilot-multi.mjs +0 -885
  330. package/scripts/backfill-learnings-expires.mjs +0 -196
  331. package/scripts/backfill-learnings.mjs +0 -203
  332. package/scripts/fleet-instruction-scan.mjs +0 -141
  333. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  334. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  335. package/scripts/lib/webhook-url.mjs +0 -105
  336. package/scripts/lifecycle-sim-v6.mjs +0 -347
  337. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  338. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  339. package/scripts/upload-social-preview.mjs +0 -316
  340. package/skills/_shared/model-selection.md +0 -64
  341. package/skills/contract-version-bump/SKILL.md +0 -219
  342. package/skills/daily/SKILL.md +0 -222
  343. package/skills/daily/generate.sh +0 -92
  344. package/skills/daily/templates/daily.md.tpl +0 -36
  345. package/skills/journey-audit/SKILL.md +0 -270
  346. package/skills/skill-creator/SKILL.md +0 -168
  347. package/skills/ubiquitous-language/SKILL.md +0 -97
  348. package/skills/vault-sync/package-lock.json +0 -40
  349. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  350. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * post-edit-import-probe.mjs — PostToolUse hook that catches a BROKEN
4
+ * hook-reachable module the moment it is saved (GitLab #1224).
5
+ *
6
+ * Incident 2026-09-04: `scripts/lib/session-identity/own-session.mjs` was saved
7
+ * with a call to a function that was written two edits later. Three hook entry
8
+ * files import that module on every tool call, so every Bash/Edit/Write call
9
+ * across the host — including two foreign sessions — got "Internal hook error —
10
+ * request blocked" for ~8 minutes. The defect was invisible to `node --check`
11
+ * (syntactically valid) and to a plain `await import()` (module-level
12
+ * evaluation succeeds; the ReferenceError is raised only when the exported
13
+ * function is CALLED). ESLint `no-undef` catches it — that is the primary check
14
+ * here; the import probe is the secondary net for load-time throws.
15
+ *
16
+ * Decision flow (each gate exits 0 silently):
17
+ * G1 profile gate — shouldRunHook('post-edit-import-probe')
18
+ * G2 stdin JSON (null-safe)
19
+ * G3 tool_name ∈ {Edit, Write, MultiEdit}
20
+ * G4 tool_input.file_path present
21
+ * G5 extension ∈ {.mjs, .js, .cjs}
22
+ * G6 membership in hooks/_lib/hook-import-set.json (the committed
23
+ * hook-reachable allowlist) — anything else is not this probe's business
24
+ * C1 PRIMARY: ESLint on the single file; only `no-undef` and fatal/parse
25
+ * errors count as a failure (other rules belong to `npm run lint`)
26
+ * C2 SECONDARY (scripts/lib/** only — NEVER hooks/*.mjs, half of which run
27
+ * main() at module bottom): `await import(file)` in a child process
28
+ *
29
+ * The PRIMARY check needs an ESLint install in EITHER the project's own
30
+ * `node_modules` (tried first — an npm-installed consumer repo has no plugin
31
+ * devDeps) or the plugin root's; with neither, C1 degrades to a silent no-op
32
+ * and only C2 remains. `SO_IMPORT_PROBE_ESLINT` overrides that resolution.
33
+ *
34
+ * Output on failure: an `additionalContext` roll-up + a `systemMessage`.
35
+ * Exit codes: 0 ALWAYS — a probe for hook breakage must never become one
36
+ * (the probe's own internal faults go to stderr, never to the tool call).
37
+ */
38
+
39
+ import { shouldRunHook } from './_lib/profile-gate.mjs';
40
+ // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
41
+ if (!shouldRunHook('post-edit-import-probe')) process.exit(0);
42
+
43
+ import path from 'node:path';
44
+ import { readFileSync, existsSync } from 'node:fs';
45
+ import { spawnSync } from 'node:child_process';
46
+ import { performance } from 'node:perf_hooks';
47
+
48
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
49
+
50
+ /** Tool names whose payload carries an edited file path. */
51
+ const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
52
+
53
+ /** Extensions this probe can reason about (Node ESM/CJS modules). */
54
+ const MODULE_EXTS = new Set(['.mjs', '.js', '.cjs']);
55
+
56
+ /**
57
+ * Total wall-clock budget for BOTH checks. The hooks.json ceiling is 5 s; the
58
+ * measured ESLint single-file run is 0.3–0.5 s, so 3 s leaves headroom for a
59
+ * cold start without ever approaching the harness timeout.
60
+ */
61
+ const BUDGET_MS = 3_000;
62
+
63
+ /** The plugin's own root (…/hooks/.. ), used to locate a vendored ESLint. */
64
+ const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..');
65
+
66
+ /**
67
+ * Read stdin to EOF (best-effort). Returns parsed JSON or null on any failure.
68
+ *
69
+ * @returns {Promise<object|null>}
70
+ */
71
+ function readStdinJson() {
72
+ return new Promise((resolve) => {
73
+ if (process.stdin.readableEnded || process.stdin.closed) { resolve(null); return; }
74
+ const chunks = [];
75
+ const timer = setTimeout(() => resolve(null), 5_000);
76
+ process.stdin.setEncoding('utf8');
77
+ process.stdin.on('data', (c) => chunks.push(c));
78
+ process.stdin.on('end', () => {
79
+ clearTimeout(timer);
80
+ const raw = chunks.join('').trim();
81
+ if (!raw) { resolve(null); return; }
82
+ try { resolve(JSON.parse(raw)); } catch { resolve(null); }
83
+ });
84
+ process.stdin.on('error', () => { clearTimeout(timer); resolve(null); });
85
+ process.stdin.resume();
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Load the committed hook-reachable allowlist.
91
+ * Missing / unreadable / malformed → empty map (silent no-op, never a crash).
92
+ *
93
+ * @param {string} projectDir
94
+ * @returns {Map<string, string[]>} repo-relative path → hook entry basenames
95
+ */
96
+ export function loadImportSet(projectDir) {
97
+ const file = path.join(projectDir, 'hooks', '_lib', 'hook-import-set.json');
98
+ try {
99
+ if (!existsSync(file)) return new Map();
100
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
101
+ const entries = Array.isArray(parsed?.entries) ? parsed.entries : [];
102
+ const map = new Map();
103
+ for (const e of entries) {
104
+ if (e && typeof e.file === 'string') {
105
+ map.set(e.file, Array.isArray(e.reachable_from) ? e.reachable_from : []);
106
+ }
107
+ }
108
+ return map;
109
+ } catch {
110
+ return new Map();
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Repo-relative POSIX path, or null when the file lives outside the project.
116
+ *
117
+ * @param {string} projectDir
118
+ * @param {string} absFile
119
+ * @returns {string|null}
120
+ */
121
+ export function toRepoRelative(projectDir, absFile) {
122
+ const rel = path.relative(projectDir, absFile);
123
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
124
+ return rel.split(path.sep).join('/');
125
+ }
126
+
127
+ /**
128
+ * Locate an ESLint entry script. The PROJECT's own install is tried first —
129
+ * in an npm-installed consumer repo the plugin root has no devDependencies at
130
+ * all, so a plugin-root-only lookup makes the primary check a silent no-op
131
+ * there. Both the `.bin` shim and the package's own entry script are probed at
132
+ * each root (a pnpm/npm layout may publish either).
133
+ *
134
+ * `SO_IMPORT_PROBE_ESLINT` overrides the search: an explicit path is used as
135
+ * given, and an EMPTY value suppresses the check (test seam for the
136
+ * eslint-unavailable path).
137
+ *
138
+ * Returns null when nothing is found → the check is skipped silently: a
139
+ * consumer repo without ESLint installed is not a defect.
140
+ *
141
+ * @param {string} projectDir
142
+ * @returns {string|null}
143
+ */
144
+ export function resolveEslintBin(projectDir) {
145
+ const override = process.env.SO_IMPORT_PROBE_ESLINT;
146
+ if (typeof override === 'string') {
147
+ if (!override) return null;
148
+ try { return existsSync(override) ? override : null; } catch { return null; }
149
+ }
150
+ const candidates = [];
151
+ for (const root of [projectDir, PLUGIN_ROOT]) {
152
+ candidates.push(path.join(root, 'node_modules', '.bin', 'eslint'));
153
+ candidates.push(path.join(root, 'node_modules', 'eslint', 'bin', 'eslint.js'));
154
+ }
155
+ for (const c of candidates) {
156
+ try { if (existsSync(c)) return c; } catch { /* unreadable → next candidate */ }
157
+ }
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * PRIMARY check — run ESLint on the single file and keep only the message
163
+ * classes that indicate a module which will THROW when a hook calls into it:
164
+ * `no-undef` (the 2026-09-04 incident shape) and any fatal/parse error.
165
+ *
166
+ * @param {string} projectDir
167
+ * @param {string} absFile
168
+ * @param {number} timeoutMs
169
+ * @returns {string|null} first offending message, or null when clean/skipped
170
+ */
171
+ export function runEslintCheck(projectDir, absFile, timeoutMs) {
172
+ const bin = resolveEslintBin(projectDir);
173
+ if (!bin) {
174
+ // ESLint unavailable → skip, never guess. Traced so a consumer repo can
175
+ // tell "primary check found nothing" from "primary check never ran".
176
+ if (process.env.SO_IMPORT_PROBE_TRACE) process.stderr.write('probe:eslint-unavailable\n');
177
+ return null;
178
+ }
179
+
180
+ const res = spawnSync(
181
+ process.execPath,
182
+ [bin, '--no-warn-ignored', '--format', 'json', absFile],
183
+ { cwd: projectDir, encoding: 'utf8', timeout: timeoutMs },
184
+ );
185
+ // A crashed/timed-out ESLint produces no parseable report — skip rather than
186
+ // invent a finding. Same for a repo whose flat config cannot be resolved.
187
+ let report;
188
+ try { report = JSON.parse(res.stdout ?? ''); } catch { return null; }
189
+ if (!Array.isArray(report)) return null;
190
+
191
+ for (const fileResult of report) {
192
+ for (const m of fileResult?.messages ?? []) {
193
+ const isFatal = m.fatal === true;
194
+ const isUndef = m.ruleId === 'no-undef';
195
+ if (isFatal || isUndef) {
196
+ return `L${m.line ?? '?'} ${m.ruleId ?? 'parse-error'}: ${m.message}`;
197
+ }
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+
203
+ /**
204
+ * SECONDARY check — actually import the module in a child process to catch a
205
+ * load-time throw. Restricted to `scripts/lib/**`: roughly half the
206
+ * `hooks/*.mjs` entry files call `main()` at module bottom, so importing one
207
+ * would EXECUTE it.
208
+ *
209
+ * @param {string} absFile
210
+ * @param {number} timeoutMs
211
+ * @returns {string|null} first error line, or null when clean
212
+ */
213
+ export function runImportCheck(absFile, timeoutMs) {
214
+ const res = spawnSync(
215
+ process.execPath,
216
+ [
217
+ '--input-type=module',
218
+ '-e',
219
+ "const { pathToFileURL } = await import('node:url');"
220
+ + ' await import(pathToFileURL(process.env.SO_IMPORT_PROBE_TARGET).href);',
221
+ ],
222
+ {
223
+ encoding: 'utf8',
224
+ timeout: timeoutMs,
225
+ // NO positional argument: a target passed as argv[1] satisfies the
226
+ // `import.meta.url === file://${process.argv[1]}` main-guard that many
227
+ // modules carry, so the probe would EXECUTE their CLI main() (measured:
228
+ // scripts/lib/sunset/walker.mjs walks the repo for ~3.9 s and is then
229
+ // SIGTERMed at the budget). The path travels in the environment instead.
230
+ // The env is minimal and deliberately drops NODE_OPTIONS — an inherited
231
+ // --require/--import would run foreign code inside the probe.
232
+ env: {
233
+ PATH: process.env.PATH ?? '',
234
+ HOME: process.env.HOME ?? '',
235
+ SO_HOOK_PROFILE: 'off',
236
+ SO_IMPORT_PROBE_TARGET: absFile,
237
+ },
238
+ },
239
+ );
240
+ if (res.status === 0) return null;
241
+ // A timeout (killed child, status null) is not evidence of breakage.
242
+ if (res.status === null) return null;
243
+ // Node echoes the offending SOURCE line before the diagnostic, and that echo
244
+ // also contains the word "Error" — anchor on the diagnostic shape
245
+ // ("ReferenceError: x is not defined") so the report names the fault, not the
246
+ // source text that triggered it.
247
+ const lines = String(res.stderr ?? '').split('\n').map((s) => s.trim()).filter(Boolean);
248
+ const diagnostic = lines.find((s) => /^[A-Za-z]*Error(:|\b)/.test(s));
249
+ return diagnostic || lines.find((s) => /Error/.test(s)) || `import failed with exit ${res.status}`;
250
+ }
251
+
252
+ /**
253
+ * Build the operator-facing warning. It names the blast radius explicitly —
254
+ * the whole point of the probe is that a broken module here is not a local
255
+ * defect.
256
+ *
257
+ * @param {string} relPath
258
+ * @param {string} error
259
+ * @param {string[]} reachableFrom
260
+ * @returns {string}
261
+ */
262
+ export function buildWarning(relPath, error, reachableFrom) {
263
+ const hooks = reachableFrom.length > 0 ? reachableFrom.join(', ') : '(unknown)';
264
+ return (
265
+ `⚠ import-probe: ${relPath} — ${error}. ` +
266
+ `This module is reached by hooks ${hooks} — every Edit/Write/Bash call may now be ` +
267
+ `blocked host-wide. Fix before continuing.`
268
+ );
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // main
273
+ // ---------------------------------------------------------------------------
274
+
275
+ async function main() {
276
+ const started = performance.now();
277
+ const input = await readStdinJson();
278
+ if (!input) return; // G2
279
+
280
+ const toolName = typeof input.tool_name === 'string' ? input.tool_name : null;
281
+ if (!toolName || !EDIT_TOOLS.has(toolName)) return; // G3
282
+
283
+ const toolInput = input.tool_input;
284
+ const filePath = toolInput && typeof toolInput.file_path === 'string'
285
+ ? toolInput.file_path : null;
286
+ if (!filePath) return; // G4
287
+
288
+ if (!MODULE_EXTS.has(path.extname(filePath).toLowerCase())) return; // G5
289
+
290
+ const projectDir = getProjectDir();
291
+ const absFile = path.resolve(projectDir, filePath);
292
+ const rel = toRepoRelative(projectDir, absFile);
293
+ if (!rel) return;
294
+
295
+ const importSet = loadImportSet(projectDir);
296
+ if (!importSet.has(rel)) return; // G6
297
+ const reachableFrom = importSet.get(rel) ?? [];
298
+
299
+ // Trace hook for tests: proves the gates above short-circuit BEFORE any spawn.
300
+ if (process.env.SO_IMPORT_PROBE_TRACE) process.stderr.write('probe:checks-start\n');
301
+
302
+ let error = runEslintCheck(projectDir, absFile, BUDGET_MS); // C1
303
+ let check = 'eslint';
304
+
305
+ if (!error && rel.startsWith('scripts/lib/')) { // C2
306
+ // spawnSync rejects a fractional timeout (RangeError) — round it.
307
+ const remaining = Math.max(250, Math.round(BUDGET_MS - (performance.now() - started)));
308
+ error = runImportCheck(absFile, remaining);
309
+ if (error) check = 'import';
310
+ }
311
+
312
+ if (!error) return;
313
+
314
+ const durationMs = Math.round(performance.now() - started);
315
+ try {
316
+ const { emitEvent } = await import('../scripts/lib/events.mjs');
317
+ await emitEvent('orchestrator.hook.import_probe_failed', {
318
+ file: rel,
319
+ check,
320
+ error,
321
+ reachable_from: reachableFrom,
322
+ duration_ms: durationMs,
323
+ });
324
+ } catch { /* best-effort telemetry — never blocks the hook */ }
325
+
326
+ process.stdout.write(JSON.stringify({
327
+ hookSpecificOutput: {
328
+ hookEventName: 'PostToolUse',
329
+ additionalContext: buildWarning(rel, error, reachableFrom),
330
+ },
331
+ systemMessage: `⚠ import-probe FAILED: ${rel}`,
332
+ }));
333
+ }
334
+
335
+ // Exit 0 always — a hook that guards against hook breakage must never block.
336
+ // The catch stays silent towards the TOOL CALL but not towards the operator:
337
+ // a probe whose own breakage is invisible is a probe nobody can trust.
338
+ main()
339
+ .catch((err) => {
340
+ try {
341
+ process.stderr.write(`post-edit-import-probe: ${err?.message ?? String(err)}\n`);
342
+ } catch { /* stderr gone — nothing left to report to */ }
343
+ })
344
+ .finally(() => process.exit(0));