session-orchestrator 3.23.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 (393) 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 +13 -0
  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 +1401 -0
  81. package/NOTICE +11 -6
  82. package/README.md +127 -92
  83. package/agents/db-specialist.md +0 -1
  84. package/agents/eval-judge.md +1 -1
  85. package/agents/skill-applied-judge.md +1 -1
  86. package/assets/wave-lifecycle.svg +98 -0
  87. package/commands/release.md +6 -3
  88. package/commands/session.md +18 -3
  89. package/docs/README.md +4 -0
  90. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  91. package/docs/baseline.md +67 -0
  92. package/docs/ci-setup.md +249 -48
  93. package/docs/codex-setup.md +66 -22
  94. package/docs/components.md +37 -16
  95. package/docs/cursor-setup.md +6 -2
  96. package/docs/events-schema.md +51 -10
  97. package/docs/instruction-delivery.md +62 -0
  98. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  99. package/docs/migration-v4.md +341 -0
  100. package/docs/pi-setup.md +6 -1
  101. package/docs/plugin-architecture-v3.md +1 -1
  102. package/docs/rule-authoring.md +85 -19
  103. package/docs/scope-collision-guard.md +8 -8
  104. package/docs/session-config-reference.md +120 -61
  105. package/docs/session-config-template.md +40 -33
  106. package/docs/telemetry/telemetry-claims.md +11 -10
  107. package/docs/telemetry.md +187 -4
  108. package/docs/vault-docs-architecture.md +50 -11
  109. package/hooks/_lib/atomic-json.mjs +111 -0
  110. package/hooks/_lib/hook-import-set.json +1487 -0
  111. package/hooks/_lib/subagent-paths.mjs +143 -0
  112. package/hooks/_lib/subagent-transcript.mjs +562 -0
  113. package/hooks/config-protection.mjs +2 -2
  114. package/hooks/cwd-change-restore.mjs +11 -31
  115. package/hooks/enforce-commands.mjs +69 -0
  116. package/hooks/enforce-scope.mjs +35 -6
  117. package/hooks/hooks-codex.json +1 -1
  118. package/hooks/hooks-cursor.json +10 -0
  119. package/hooks/hooks-pi.json +5 -0
  120. package/hooks/hooks.json +6 -1
  121. package/hooks/loop-guard.mjs +3 -3
  122. package/hooks/on-session-end.mjs +280 -14
  123. package/hooks/on-session-start.mjs +153 -4
  124. package/hooks/on-stop.mjs +371 -17
  125. package/hooks/operator-steer.mjs +2 -2
  126. package/hooks/post-bash-write-verify.mjs +189 -4
  127. package/hooks/post-edit-import-probe.mjs +344 -0
  128. package/hooks/post-subagent-discovery-validator.mjs +278 -392
  129. package/hooks/post-tool-batch-wave-signal.mjs +272 -44
  130. package/hooks/post-tool-failure-corrective-context.mjs +11 -34
  131. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  132. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  133. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  134. package/hooks/skill-invocation-telemetry.mjs +17 -5
  135. package/hooks/subagent-telemetry.mjs +24 -30
  136. package/monitors/monitors.json +3 -3
  137. package/package.json +9 -1
  138. package/pi/prompts/session.md +2 -2
  139. package/plugin.json +27 -0
  140. package/scripts/autopilot.mjs +26 -12
  141. package/scripts/backfill-abandoned-sessions.mjs +130 -15
  142. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  143. package/scripts/dialectic-deriver.mjs +73 -8
  144. package/scripts/emit-event.mjs +10 -2
  145. package/scripts/export-hw-learnings.mjs +113 -1
  146. package/scripts/generate-agents-skills.mjs +378 -0
  147. package/scripts/generate-cursor-adapter.mjs +45 -8
  148. package/scripts/generate-hook-import-set.mjs +249 -0
  149. package/scripts/lib/agent-status.mjs +13 -2
  150. package/scripts/lib/auq/parse.mjs +5 -29
  151. package/scripts/lib/auto-dialectic.mjs +68 -0
  152. package/scripts/lib/auto-dream.mjs +38 -36
  153. package/scripts/lib/autonomy/suitability.mjs +6 -0
  154. package/scripts/lib/autopilot/loop.mjs +2 -2
  155. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  156. package/scripts/lib/build-live-signals.mjs +25 -22
  157. package/scripts/lib/ci-status-banner.mjs +220 -75
  158. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  159. package/scripts/lib/cold-start-detector.mjs +23 -14
  160. package/scripts/lib/config/auto-dream.mjs +2 -1
  161. package/scripts/lib/config/block-header.mjs +63 -0
  162. package/scripts/lib/config/block-preprocess.mjs +177 -0
  163. package/scripts/lib/config/broken-window.mjs +2 -1
  164. package/scripts/lib/config/cold-start.mjs +2 -1
  165. package/scripts/lib/config/config-protection.mjs +22 -2
  166. package/scripts/lib/config/context-coverage.mjs +2 -1
  167. package/scripts/lib/config/cross-repo.mjs +2 -1
  168. package/scripts/lib/config/custom-phases.mjs +2 -1
  169. package/scripts/lib/config/dialectic.mjs +2 -1
  170. package/scripts/lib/config/discovery-validator.mjs +9 -3
  171. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  172. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  173. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  174. package/scripts/lib/config/docs-staleness.mjs +2 -1
  175. package/scripts/lib/config/drift-check.mjs +2 -1
  176. package/scripts/lib/config/eval.mjs +2 -1
  177. package/scripts/lib/config/events-rotation.mjs +2 -1
  178. package/scripts/lib/config/evolve.mjs +8 -2
  179. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  180. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  181. package/scripts/lib/config/handover-gate.mjs +2 -1
  182. package/scripts/lib/config/health-endpoints.mjs +388 -0
  183. package/scripts/lib/config/issue-budget.mjs +2 -1
  184. package/scripts/lib/config/loop-guard.mjs +2 -1
  185. package/scripts/lib/config/memory.mjs +2 -1
  186. package/scripts/lib/config/moc-staleness.mjs +2 -1
  187. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  188. package/scripts/lib/config/private-config-dir.mjs +67 -0
  189. package/scripts/lib/config/reconcile.mjs +2 -1
  190. package/scripts/lib/config/remote-hosts.mjs +234 -0
  191. package/scripts/lib/config/section-extractor.mjs +7 -1
  192. package/scripts/lib/config/skill-evolution.mjs +2 -1
  193. package/scripts/lib/config/slopcheck.mjs +2 -1
  194. package/scripts/lib/config/state-md-lock.mjs +2 -1
  195. package/scripts/lib/config/templates-first.mjs +2 -1
  196. package/scripts/lib/config/test.mjs +2 -1
  197. package/scripts/lib/config/vault-integration.mjs +7 -1
  198. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  199. package/scripts/lib/config/vault-staleness.mjs +2 -1
  200. package/scripts/lib/config/vault-sync.mjs +2 -1
  201. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  202. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  203. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  204. package/scripts/lib/config.mjs +31 -3
  205. package/scripts/lib/convergence-monitor.mjs +82 -16
  206. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  207. package/scripts/lib/dispatcher/rank.mjs +124 -48
  208. package/scripts/lib/ecosystem-health.mjs +16 -2
  209. package/scripts/lib/eval/engine.mjs +9 -1
  210. package/scripts/lib/eval/session-resolve.mjs +23 -4
  211. package/scripts/lib/events-schema.mjs +48 -0
  212. package/scripts/lib/events.mjs +256 -7
  213. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  214. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  215. package/scripts/lib/frontmatter-guard.mjs +131 -13
  216. package/scripts/lib/gates/gate-full.mjs +26 -0
  217. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  218. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  219. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  220. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  221. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  222. package/scripts/lib/host-identity.mjs +50 -11
  223. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  224. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  225. package/scripts/lib/learnings/io.mjs +60 -6
  226. package/scripts/lib/memory-banner.mjs +20 -8
  227. package/scripts/lib/memory-proposals/store.mjs +30 -22
  228. package/scripts/lib/owner-config-banner.mjs +43 -6
  229. package/scripts/lib/owner-config-loader.mjs +21 -10
  230. package/scripts/lib/owner-interview.mjs +3 -3
  231. package/scripts/lib/owner-yaml.mjs +207 -14
  232. package/scripts/lib/peer-discovery.mjs +20 -2
  233. package/scripts/lib/platform.mjs +108 -15
  234. package/scripts/lib/plugin-update-banner.mjs +406 -0
  235. package/scripts/lib/project-hygiene.mjs +38 -2
  236. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  237. package/scripts/lib/quality-gate.mjs +133 -44
  238. package/scripts/lib/reconcile/emitter.mjs +68 -6
  239. package/scripts/lib/reconcile/engine.mjs +249 -9
  240. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  241. package/scripts/lib/reconcile/writer.mjs +40 -18
  242. package/scripts/lib/scope-gate.mjs +36 -0
  243. package/scripts/lib/session-close-backfill.mjs +125 -18
  244. package/scripts/lib/session-discovery.mjs +57 -3
  245. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  246. package/scripts/lib/session-id.mjs +12 -23
  247. package/scripts/lib/session-identity/own-session.mjs +187 -11
  248. package/scripts/lib/session-lock-shape.mjs +43 -0
  249. package/scripts/lib/session-lock.mjs +5 -10
  250. package/scripts/lib/session-registry.mjs +25 -9
  251. package/scripts/lib/session-schema/constants.mjs +36 -2
  252. package/scripts/lib/session-schema/validator.mjs +38 -4
  253. package/scripts/lib/session-start-probes.mjs +18 -1
  254. package/scripts/lib/session-transition.mjs +1 -1
  255. package/scripts/lib/sessions-canonical.mjs +446 -0
  256. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  257. package/scripts/lib/skill-health/join.mjs +17 -4
  258. package/scripts/lib/state-md.mjs +78 -0
  259. package/scripts/lib/sunset/walker.mjs +6 -0
  260. package/scripts/lib/telemetry/schema.mjs +255 -17
  261. package/scripts/lib/telemetry/sync.mjs +417 -24
  262. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  263. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  264. package/scripts/lib/validate/check-agents.mjs +3 -3
  265. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  266. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  267. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  268. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  269. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  270. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  271. package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
  272. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  273. package/scripts/lib/validate/check-unwired-features.mjs +0 -9
  274. package/scripts/lib/validate/check-validator-registration.mjs +254 -0
  275. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  276. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  277. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  278. package/scripts/lib/vault-backfill/template.mjs +63 -6
  279. package/scripts/lib/vault-mirror/process.mjs +165 -42
  280. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  281. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  282. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  283. package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
  284. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  285. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  286. package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
  287. package/scripts/lib/wave-resource-gate.mjs +133 -7
  288. package/scripts/lib/wave-sizing.mjs +4 -1
  289. package/scripts/lib/wave-transcript-tail.mjs +142 -8
  290. package/scripts/materialize-wave-scope.mjs +32 -9
  291. package/scripts/memory-propose.mjs +146 -8
  292. package/scripts/migrate-cold-start-seed.mjs +4 -1
  293. package/scripts/parse-config.mjs +60 -3
  294. package/scripts/promote-vault-strict.mjs +4 -15
  295. package/scripts/release.mjs +337 -29
  296. package/scripts/repair-invalid-sessions.mjs +3 -3
  297. package/scripts/run-quality-gate.mjs +128 -11
  298. package/scripts/site-numbers.mjs +36 -4
  299. package/scripts/sweep-expired-learnings.mjs +90 -0
  300. package/scripts/sync-vault-schema.mjs +3 -1
  301. package/scripts/telemetry.mjs +2 -2
  302. package/scripts/validate-plugin.mjs +187 -0
  303. package/scripts/validate-wave-scope.mjs +28 -8
  304. package/scripts/vault-consolidate.mjs +3 -11
  305. package/scripts/vault-integration-watcher.mjs +2 -4
  306. package/scripts/vault-mirror.mjs +111 -26
  307. package/scripts/wave-scope-binding.mjs +215 -0
  308. package/skills/_shared/instruction-file-resolution.md +10 -0
  309. package/skills/_shared/parallel-aware-auq.md +31 -2
  310. package/skills/_shared/parallel-aware-preamble.md +18 -4
  311. package/skills/_shared/platform-tools.md +1 -1
  312. package/skills/_shared/state-ownership.md +1 -1
  313. package/skills/architecture/SKILL.md +7 -5
  314. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  315. package/skills/autopilot/SKILL.md +4 -18
  316. package/skills/claude-md-drift-check/SKILL.md +5 -1
  317. package/skills/claude-md-drift-check/checker.mjs +62 -2
  318. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  319. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  320. package/skills/discovery/probes-arch.md +20 -18
  321. package/skills/dispatcher/SKILL.md +3 -2
  322. package/skills/ecosystem-health/SKILL.md +4 -1
  323. package/skills/ecosystem-health/wizard.md +5 -0
  324. package/skills/evolve/SKILL.md +87 -11
  325. package/skills/frontmatter-guard/SKILL.md +11 -5
  326. package/skills/npm-publish/SKILL.md +1 -1
  327. package/skills/reconcile/SKILL.md +38 -2
  328. package/skills/remote-offload/SKILL.md +89 -0
  329. package/skills/session-end/SKILL.md +18 -905
  330. package/skills/session-end/phase-3-6-tail.md +19 -9
  331. package/skills/session-end/plan-verification.md +221 -155
  332. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  333. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  334. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  335. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  336. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  337. package/skills/session-end/references/session-summary-template.md +62 -0
  338. package/skills/session-plan/SKILL.md +49 -0
  339. package/skills/session-start/SKILL.md +41 -900
  340. package/skills/session-start/phase-8-5-express-path.md +1 -1
  341. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  342. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  343. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  344. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  345. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  346. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  347. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  348. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  349. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  350. package/skills/vault-sync/validator.mjs +21 -27
  351. package/skills/wave-executor/SKILL.md +16 -2
  352. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  353. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  354. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  355. package/skills/wave-executor/wave-loop.md +14 -1271
  356. package/templates/_shared/journey-manifest.md +10 -6
  357. package/.cursor/commands/autopilot-multi.md +0 -14
  358. package/.cursor/commands/contract-version-bump.md +0 -14
  359. package/.cursor/commands/journey-audit.md +0 -14
  360. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  361. package/.cursor/skills/daily/SKILL.md +0 -12
  362. package/.cursor/skills/domain-model/SKILL.md +0 -13
  363. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  364. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  365. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  366. package/commands/autopilot-multi.md +0 -74
  367. package/commands/contract-version-bump.md +0 -28
  368. package/commands/journey-audit.md +0 -43
  369. package/pi/prompts/autopilot-multi.md +0 -12
  370. package/pi/prompts/contract-version-bump.md +0 -12
  371. package/pi/prompts/journey-audit.md +0 -12
  372. package/scripts/autopilot-multi.mjs +0 -885
  373. package/scripts/backfill-learnings-expires.mjs +0 -196
  374. package/scripts/backfill-learnings.mjs +0 -203
  375. package/scripts/fleet-instruction-scan.mjs +0 -141
  376. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  377. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  378. package/scripts/lib/webhook-url.mjs +0 -105
  379. package/scripts/lifecycle-sim-v6.mjs +0 -347
  380. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  381. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  382. package/scripts/upload-social-preview.mjs +0 -316
  383. package/skills/_shared/model-selection.md +0 -64
  384. package/skills/contract-version-bump/SKILL.md +0 -219
  385. package/skills/daily/SKILL.md +0 -222
  386. package/skills/daily/generate.sh +0 -92
  387. package/skills/daily/templates/daily.md.tpl +0 -36
  388. package/skills/journey-audit/SKILL.md +0 -269
  389. package/skills/skill-creator/SKILL.md +0 -168
  390. package/skills/ubiquitous-language/SKILL.md +0 -97
  391. package/skills/vault-sync/package-lock.json +0 -40
  392. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  393. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * post-bash-write-verify.mjs — PostToolUse hook (matcher `Bash`): report
4
- * working-tree changes a Bash call made OUTSIDE the wave's `allowedPaths`.
4
+ * working-tree changes a Bash call made OUTSIDE the wave's `allowedPaths` —
5
+ * except a path a PEER session declared through a `peer-session-*` record in
6
+ * the wave's aggregate scope sidecar (#1195), which is named as a peer write.
5
7
  *
6
8
  * ## Why this hook exists (#915, follow-up to #906 / #800)
7
9
  *
@@ -160,7 +162,14 @@ import { findScopeFile, pathMatchesPattern } from '../scripts/lib/hardening.mjs'
160
162
  // tests/hooks/post-bash-write-verify.test.mjs, which imports the named export —
161
163
  // is unchanged. Two byte-identical copies of a clock is exactly the one-fact-two-
162
164
  // copies class this repo keeps paying for.
163
- import { sessionAgeMs } from '../scripts/lib/scope-gate.mjs';
165
+ import { sessionAgeMs, PEER_RECORD_PREFIX, isPeerRecordId } from '../scripts/lib/scope-gate.mjs';
166
+ // #1153 P1 — the same process-local ownership check hooks/enforce-scope.mjs
167
+ // applies at Gate 3b. This hook reads the SAME working-copy `wave-scope.json`,
168
+ // so without it a peer session's manifest drives this session's advisories.
169
+ import {
170
+ readProcessLocalSessionIds,
171
+ classifyManifestSession,
172
+ } from '../scripts/lib/session-identity/own-session.mjs';
164
173
 
165
174
  // ---------------------------------------------------------------------------
166
175
  // Constants
@@ -322,6 +331,90 @@ export function isInScope(relPath, allowedPaths) {
322
331
  return allowedPaths.some((p) => typeof p === 'string' && pathMatchesPattern(relPath, p));
323
332
  }
324
333
 
334
+ /**
335
+ * Re-exported for readers of this hook: the prefix that marks an
336
+ * aggregate-sidecar record as a PEER SESSION's declared scope rather than one
337
+ * of this wave's own agents (#1195). The DEFINITION lives in
338
+ * `scripts/lib/scope-gate.mjs`, next to `unionFileScopes`, which must exclude
339
+ * exactly these records from `allowedPaths` for the peer branch below to be
340
+ * reachable at all.
341
+ */
342
+ export { PEER_RECORD_PREFIX };
343
+
344
+ /**
345
+ * The peer-session records of a wave's aggregate scope sidecar.
346
+ *
347
+ * Shape, as written by `scripts/materialize-wave-scope.mjs`
348
+ * (`<state-dir>/filescopes/wave-<N>.scopes.json`): ONE JSON array of
349
+ * `{ id, files }` records — the per-agent files under
350
+ * `<state-dir>/filescopes/wave-<N>/<id>.json` are path STRINGS and are a
351
+ * different artefact (see `docs/scope-collision-guard.md` § 2.2). Only records
352
+ * whose `id` starts with `peer-session-` are returned: those are paths a peer
353
+ * session announced and this session's coordinator carried into the manifest
354
+ * union (`skills/wave-executor/wave-loop.md` § Scope Manifest).
355
+ *
356
+ * Absent, unreadable or malformed sidecar ⇒ `[]`, which restores the exact
357
+ * pre-#1195 behaviour. Never throws.
358
+ *
359
+ * @param {string} stateDir absolute path of the harness state dir (the
360
+ * directory holding `wave-scope.json`)
361
+ * @param {unknown} wave the manifest's `wave` field
362
+ * @returns {Array<{ id: string, files: string[] }>}
363
+ */
364
+ export function readPeerScopeRecords(stateDir, wave) {
365
+ if (typeof stateDir !== 'string' || stateDir === '') return [];
366
+ if (typeof wave !== 'number' || !Number.isInteger(wave) || wave <= 0) return [];
367
+ let parsed;
368
+ try {
369
+ parsed = JSON.parse(
370
+ readFileSync(path.join(stateDir, 'filescopes', `wave-${wave}.scopes.json`), 'utf8'),
371
+ );
372
+ } catch {
373
+ return [];
374
+ }
375
+ if (!Array.isArray(parsed)) return [];
376
+ return parsed
377
+ .filter(
378
+ (r) => r && typeof r === 'object'
379
+ && isPeerRecordId(r.id)
380
+ && Array.isArray(r.files),
381
+ )
382
+ .map((r) => ({ id: r.id, files: r.files.filter((f) => typeof f === 'string') }));
383
+ }
384
+
385
+ /**
386
+ * Which peer record — if any — declared this path?
387
+ *
388
+ * Same matcher as `isInScope`, so a peer's declaration is read exactly the way
389
+ * the wave's own `allowedPaths` are.
390
+ *
391
+ * @param {string} relPath
392
+ * @param {Array<{ id: string, files: string[] }>} peerRecords
393
+ * @returns {string|null} the peer record id, or null
394
+ */
395
+ export function peerRecordFor(relPath, peerRecords) {
396
+ if (!Array.isArray(peerRecords)) return null;
397
+ for (const record of peerRecords) {
398
+ if (record.files.some((p) => pathMatchesPattern(relPath, p))) return record.id;
399
+ }
400
+ return null;
401
+ }
402
+
403
+ /**
404
+ * Render the peer-write notice (#1195) — deliberately a NOTICE and not part of
405
+ * the violation report: a peer record is EXCLUDED from the union that computes
406
+ * `allowedPaths` (`unionFileScopes`, `scripts/lib/scope-gate.mjs`), so a peer
407
+ * path is outside `allowedPaths` and reaches this branch; naming it as a
408
+ * violation would re-file an agreed peer write as an alarm.
409
+ *
410
+ * @param {string} relPath
411
+ * @param {string} peerId the `peer-session-<id>` record id
412
+ * @returns {string}
413
+ */
414
+ export function formatPeerWriteNotice(relPath, peerId) {
415
+ return `bash-write-verify: ${relPath} inside ${peerId} scope — peer write, not a violation`;
416
+ }
417
+
325
418
  /**
326
419
  * Stable signature of the wave's scope. A change means a new wave, which must
327
420
  * re-baseline silently rather than blame the next Bash call for the previous
@@ -768,6 +861,17 @@ async function main() {
768
861
  }
769
862
  if (raw === null) return; // file vanished between findScopeFile and read
770
863
 
864
+ // The control-file identity of THIS call, computed BEFORE the G3b
865
+ // stand-down (W4/F7). Ordering is the whole fix: G3b used to `return` above
866
+ // this, so a peer or agent that REBOUND `wave-scope.json` to a fabricated
867
+ // `session_id` with one `cat >` redirect disarmed this session's gates AND
868
+ // suppressed the #938 control-file notice at the same time. Deleting the file
869
+ // was reported (G3 above); rebinding it was not — the louder half of the
870
+ // tamper trail was the one behind the gate the tamper opens.
871
+ //
872
+ // Nothing but the hash/enforcement snapshot is derived here: no path
873
+ // evaluation, no allowedPaths read, no report — those still wait for the
874
+ // classification below to judge the manifest ours.
771
875
  const relScopePath = path.relative(repoRoot, scopePath) || scopePath;
772
876
  const currentScopeState = {
773
877
  hash: createHash('sha1').update(raw).digest('hex').slice(0, 16),
@@ -777,6 +881,73 @@ async function main() {
777
881
  gateOn: scope ? scope?.gates?.['bash-write-verify'] !== false : true,
778
882
  };
779
883
 
884
+ // G3b (#1153 P1) — is this manifest even MINE?
885
+ //
886
+ // `wave-scope.json` is a WORKING-COPY artefact; two live sessions in one
887
+ // checkout read the same bytes. Everything below this point — the path
888
+ // report, the control-file notice, the snapshot — describes THIS session's
889
+ // relationship to a wave plan. When the manifest provably names another
890
+ // session, none of it is about us, and the advisories were a peer's
891
+ // `allowedPaths` scolding an unrelated session (#1082 / #1123).
892
+ //
893
+ // Deliberately AFTER the parse: an unparseable manifest leaves `scope === null`,
894
+ // which classifies as `'unknown'` and keeps every pre-#1153 behaviour —
895
+ // including the #938 tamper notices, which must never be reachable via a
896
+ // manifest a Bash call just corrupted. `'own'` and `'unknown'` (legacy,
897
+ // unbound, or no resolvable own identity) fall through unchanged.
898
+ //
899
+ // CEILING (BV-004): on a harness that supplies no session id at all (Codex
900
+ // CLI, Cursor today) the own-id set is empty, so this gate is permanently
901
+ // `'unknown'` = full pre-#1153 behaviour there.
902
+ {
903
+ const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
904
+ const { verdict, manifestIds } = classifyManifestSession(scope ?? {}, ownIds);
905
+ if (verdict === 'foreign') {
906
+ // Observability only — one event per stand-down decision, awaited so the
907
+ // append cannot be lost to the process exiting.
908
+ try {
909
+ const { emitEvent } = await import('../scripts/lib/events.mjs');
910
+ await emitEvent(
911
+ 'orchestrator.scope.foreign_session_ignored',
912
+ {
913
+ hook: 'post-bash-write-verify',
914
+ manifest: scopePath,
915
+ manifest_session: manifestIds,
916
+ own_session: [...ownIds],
917
+ wave: scope?.wave,
918
+ },
919
+ { repoRoot },
920
+ );
921
+ } catch { /* observability is best-effort — never blocks the decision */ }
922
+
923
+ // The stand-down itself is a control-file event when the binding CHANGED
924
+ // since the last call: the manifest this session was baselined against is
925
+ // now bound to someone else, which switches every scope gate off. Reported
926
+ // on the visible channel exactly once — the snapshot below records the new
927
+ // hash, so a stable foreign manifest (an ordinary peer wave) is silent from
928
+ // the next call on, and a rebind back to us reports again.
929
+ if (prevScopeState
930
+ && prevScopeState.hash !== 'absent'
931
+ && prevScopeState.hash !== currentScopeState.hash) {
932
+ writeSnapshot(snapFile, { ...carriedRecord, scopeState: currentScopeState });
933
+ emitMessages([
934
+ `bash-write-verify: control file ${relScopePath} changed its SESSION BINDING `
935
+ + `since the last Bash call (now names ${manifestIds.join(', ') || 'an unnamed session'}) — `
936
+ + 'this session\'s scope gates are standing down. '
937
+ + 'Scope-control changes are never exempt from reporting (#938).',
938
+ ], true);
939
+ }
940
+ return;
941
+ }
942
+ }
943
+
944
+ // ADVERSARIAL CEILING: `wave-scope.json` is a file ANY process in this working
945
+ // copy can write, so writing a foreign id into it is a per-file kill switch for
946
+ // this session's own gate. Accepted because "no manifest" already means allow
947
+ // (G3), so a foreign manifest grants nothing an `rm` would not — but the change
948
+ // MUST stay visible: see post-bash-write-verify's control-file notice (#938),
949
+ // which is the only thing that distinguishes this from a legitimate peer wave.
950
+
780
951
  let scopeMtimeMs = null;
781
952
  try {
782
953
  scopeMtimeMs = statSync(scopePath).mtimeMs;
@@ -915,8 +1086,22 @@ async function main() {
915
1086
  // first run is named, not silently trusted. Computed before the G4/G5 gate
916
1087
  // above so the gate cannot swallow it.
917
1088
  if (missingSnapshotNotice) messages.push(missingSnapshotNotice);
918
- if (report.length > 0) {
919
- messages.push(formatMessage(report, allowedPaths.length));
1089
+ // #1195 — a path a PEER session declared (a `peer-session-*` record in this
1090
+ // wave's aggregate sidecar) is outside `allowedPaths` because `--union`
1091
+ // EXCLUDES peer records when computing it (`unionFileScopes`,
1092
+ // `scripts/lib/scope-gate.mjs`) — that exclusion is what keeps this branch
1093
+ // reachable. Such a write is agreed, not a bypass. Split it out of the violation report and
1094
+ // name it, so the peer's file is countable without being an alarm. Sidecar
1095
+ // absent ⇒ `peerRecords` is empty ⇒ every path stays a violation, unchanged.
1096
+ const peerRecords = readPeerScopeRecords(path.dirname(scopePath), scope.wave);
1097
+ const violations = [];
1098
+ for (const relPath of report) {
1099
+ const peerId = peerRecordFor(relPath, peerRecords);
1100
+ if (peerId) messages.push(formatPeerWriteNotice(relPath, peerId));
1101
+ else violations.push(relPath);
1102
+ }
1103
+ if (violations.length > 0) {
1104
+ messages.push(formatMessage(violations, allowedPaths.length));
920
1105
  warn = true;
921
1106
  }
922
1107
  if (messages.length === 0) return; // silence is the common case (0.91 % fire rate)
@@ -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));