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
@@ -1,417 +0,0 @@
1
- // scripts/lib/autopilot/dep-graph.mjs
2
- //
3
- // Issue dependency graph + topological-sort scheduler for autopilot --multi-story.
4
- // Pure data layer — no I/O, no shell. Powers nextReady() lookups during multi-story
5
- // pipeline scheduling per "Autopilot Phase D — Per-Story Worktree Pipelines"
6
- // (#341; archived in the private Meta-Vault).
7
- //
8
- // Substrate: ADR-364 thin-slice MVP. Issue selection criterion: `status:ready` label
9
- // only (OPEN-1 decision in PRD). Cross-loop wait semantics use commit-based deps
10
- // (OPEN-4), but this module is pure-graph and doesn't enforce wait policy — the
11
- // worktree-pipeline driver is responsible for that.
12
-
13
- /**
14
- * @fileoverview Pure topological-sort dependency graph for GitLab/GitHub issues.
15
- *
16
- * Handles `blocks`/`blocked-by` relations between issues and exposes scheduling
17
- * helpers for the `--multi-story` autopilot mode. All functions are pure data
18
- * transformations with no I/O side effects.
19
- *
20
- * References:
21
- * - ADR-364 thin-slice MVP
22
- * - "Autopilot Phase D — Per-Story Worktree Pipelines" (#341; archived in the private Meta-Vault)
23
- */
24
-
25
- /**
26
- * @typedef {object} Issue
27
- * @property {number} iid - Issue IID/number
28
- * @property {string[]} blocks - Array of iids (as strings or numbers) this issue blocks
29
- * @property {string[]} blockedBy - Array of iids (as strings or numbers) blocking this issue
30
- * @property {string[]} labels - Issue labels (e.g., ["status:ready"])
31
- * @property {string} title - Issue title (for diagnostics)
32
- */
33
-
34
- /**
35
- * @typedef {object} DepGraph
36
- * @property {Map<number, Issue>} nodes - All known issues keyed by iid
37
- * @property {Map<number, Set<number>>} edges - iid → set of iids it blocks (outgoing)
38
- * @property {Map<number, Set<number>>} reverse - iid → set of iids blocking it (incoming)
39
- */
40
-
41
- // ---------------------------------------------------------------------------
42
- // Internal helpers
43
- // ---------------------------------------------------------------------------
44
-
45
- /**
46
- * Coerce a raw iid value (string or number) to a number, or return null if
47
- * the value is not a valid finite integer.
48
- *
49
- * @param {unknown} raw
50
- * @returns {number|null}
51
- */
52
- function toIid(raw) {
53
- const n = Number(raw);
54
- return Number.isFinite(n) && Number.isInteger(n) ? n : null;
55
- }
56
-
57
- /**
58
- * Safely coerce a field that should be an array of iids.
59
- * Returns an empty array when the field is null/undefined/non-array.
60
- *
61
- * @param {unknown} field
62
- * @returns {number[]}
63
- */
64
- function normalizeIidArray(field) {
65
- if (!Array.isArray(field)) return [];
66
- return field.map(toIid).filter((n) => n !== null);
67
- }
68
-
69
- // ---------------------------------------------------------------------------
70
- // buildGraph
71
- // ---------------------------------------------------------------------------
72
-
73
- /**
74
- * Build a DepGraph from an array of issues.
75
- *
76
- * Reconciles both directions of the blocks/blockedBy relationship:
77
- * - If A.blocks includes B's iid, then B.blockedBy is extended to include A.
78
- * - If A.blockedBy includes B's iid, then B.blocks is extended to include A.
79
- *
80
- * Issues referenced in blocks/blockedBy that are not present in the input
81
- * array are silently ignored after emitting a `console.warn`. The graph will
82
- * only contain nodes for issues present in the `issues` parameter.
83
- *
84
- * @param {Issue[]} issues - Flat array of issues to build the graph from.
85
- * @returns {DepGraph}
86
- */
87
- export function buildGraph(issues) {
88
- /** @type {Map<number, Issue>} */
89
- const nodes = new Map();
90
- /** @type {Map<number, Set<number>>} */
91
- const edges = new Map();
92
- /** @type {Map<number, Set<number>>} */
93
- const reverse = new Map();
94
-
95
- if (!Array.isArray(issues) || issues.length === 0) {
96
- return { nodes, edges, reverse };
97
- }
98
-
99
- // First pass: intern all issues, normalizing iid arrays defensively.
100
- for (const raw of issues) {
101
- const iid = toIid(raw?.iid);
102
- if (iid === null) {
103
- console.warn('[dep-graph] buildGraph: skipping issue with invalid iid', raw);
104
- continue;
105
- }
106
- /** @type {Issue} */
107
- const issue = {
108
- iid,
109
- blocks: normalizeIidArray(raw.blocks),
110
- blockedBy: normalizeIidArray(raw.blockedBy),
111
- labels: Array.isArray(raw.labels) ? [...raw.labels] : [],
112
- title: typeof raw.title === 'string' ? raw.title : String(raw.title ?? ''),
113
- };
114
- nodes.set(iid, issue);
115
- edges.set(iid, new Set());
116
- reverse.set(iid, new Set());
117
- }
118
-
119
- // Second pass: reconcile both directions and populate edge maps.
120
- // We first collect all declared relations, then validate both ends exist.
121
- // Collect raw declarations: [blocker, blocked] pairs.
122
- /** @type {Array<[number, number]>} */
123
- const declared = [];
124
-
125
- for (const issue of nodes.values()) {
126
- for (const target of issue.blocks) {
127
- declared.push([issue.iid, target]);
128
- }
129
- for (const source of issue.blockedBy) {
130
- declared.push([source, issue.iid]);
131
- }
132
- }
133
-
134
- // Deduplicate and validate, then write into edge maps and normalised Issue fields.
135
- const seen = new Set();
136
- for (const [blocker, blocked] of declared) {
137
- const key = `${blocker}:${blocked}`;
138
- if (seen.has(key)) continue;
139
- seen.add(key);
140
-
141
- if (blocker === blocked) {
142
- console.warn(`[dep-graph] buildGraph: self-loop on iid ${blocker} ignored`);
143
- continue;
144
- }
145
-
146
- const blockerNode = nodes.get(blocker);
147
- const blockedNode = nodes.get(blocked);
148
-
149
- if (!blockerNode) {
150
- console.warn(`[dep-graph] buildGraph: iid ${blocker} referenced but not in issues array — ignored`);
151
- continue;
152
- }
153
- if (!blockedNode) {
154
- console.warn(`[dep-graph] buildGraph: iid ${blocked} referenced but not in issues array — ignored`);
155
- continue;
156
- }
157
-
158
- // Sync normalised arrays on the Issue objects.
159
- if (!blockerNode.blocks.includes(blocked)) blockerNode.blocks.push(blocked);
160
- if (!blockedNode.blockedBy.includes(blocker)) blockedNode.blockedBy.push(blocker);
161
-
162
- // Populate edge maps.
163
- edges.get(blocker).add(blocked);
164
- reverse.get(blocked).add(blocker);
165
- }
166
-
167
- return { nodes, edges, reverse };
168
- }
169
-
170
- // ---------------------------------------------------------------------------
171
- // topologicalSort
172
- // ---------------------------------------------------------------------------
173
-
174
- /**
175
- * Perform a topological sort on the graph using Kahn's BFS algorithm.
176
- *
177
- * Tie-breaking between nodes at the same depth is resolved by ascending iid
178
- * (stable, deterministic output).
179
- *
180
- * When cycles are present, the `order` array contains only the nodes NOT
181
- * involved in any cycle (processed in topological order). The `cycles` array
182
- * contains each cycle as an array of iid numbers.
183
- *
184
- * @param {DepGraph} graph
185
- * @returns {{ order: number[], cycles: number[][] }}
186
- */
187
- export function topologicalSort(graph) {
188
- const { nodes, edges, reverse } = graph;
189
-
190
- if (nodes.size === 0) {
191
- return { order: [], cycles: [] };
192
- }
193
-
194
- // Build a mutable in-degree map.
195
- /** @type {Map<number, number>} */
196
- const inDegree = new Map();
197
- for (const iid of nodes.keys()) {
198
- inDegree.set(iid, reverse.get(iid)?.size ?? 0);
199
- }
200
-
201
- // Seed the queue with all zero-in-degree nodes, sorted by ascending iid.
202
- /** @type {number[]} */
203
- const queue = [...inDegree.entries()]
204
- .filter(([, deg]) => deg === 0)
205
- .map(([iid]) => iid)
206
- .sort((a, b) => a - b);
207
-
208
- /** @type {number[]} */
209
- const order = [];
210
-
211
- while (queue.length > 0) {
212
- // Dequeue the smallest iid (queue is always kept sorted).
213
- const iid = queue.shift();
214
- order.push(iid);
215
-
216
- // Decrement in-degree for all nodes this iid blocks.
217
- const neighbours = [...(edges.get(iid) ?? [])].sort((a, b) => a - b);
218
- for (const neighbour of neighbours) {
219
- const newDeg = (inDegree.get(neighbour) ?? 0) - 1;
220
- inDegree.set(neighbour, newDeg);
221
- if (newDeg === 0) {
222
- // Insert in sorted position to maintain ascending-iid stability.
223
- insertSorted(queue, neighbour);
224
- }
225
- }
226
- }
227
-
228
- // Any node not in `order` is part of a cycle.
229
- const inOrderSet = new Set(order);
230
- const cycleNodes = [...nodes.keys()].filter((iid) => !inOrderSet.has(iid));
231
-
232
- const cycles = cycleNodes.length > 0 ? detectCyclesFromNodes(graph, cycleNodes) : [];
233
-
234
- return { order, cycles };
235
- }
236
-
237
- /**
238
- * Insert `value` into a sorted array (ascending) in-place using binary search.
239
- *
240
- * @param {number[]} arr
241
- * @param {number} value
242
- */
243
- function insertSorted(arr, value) {
244
- let lo = 0;
245
- let hi = arr.length;
246
- while (lo < hi) {
247
- const mid = (lo + hi) >>> 1;
248
- if (arr[mid] < value) lo = mid + 1;
249
- else hi = mid;
250
- }
251
- arr.splice(lo, 0, value);
252
- }
253
-
254
- // ---------------------------------------------------------------------------
255
- // detectCycles
256
- // ---------------------------------------------------------------------------
257
-
258
- /**
259
- * Detect all strongly-connected components (SCCs) of size > 1 using an
260
- * iterative implementation of Tarjan's SCC algorithm.
261
- *
262
- * Returns each cycle as an array of iid numbers (members of the SCC),
263
- * sorted by ascending iid within each cycle. The outer array is sorted by
264
- * the minimum iid of each cycle (ascending).
265
- *
266
- * @param {DepGraph} graph
267
- * @returns {number[][]}
268
- */
269
- export function detectCycles(graph) {
270
- return detectCyclesFromNodes(graph, [...graph.nodes.keys()]);
271
- }
272
-
273
- /**
274
- * Internal: run Tarjan's iterative SCC algorithm over a subset of nodes
275
- * (or all nodes). Only SCCs of size > 1 are returned (true cycles).
276
- *
277
- * @param {DepGraph} graph
278
- * @param {number[]} nodeSubset - iids to consider
279
- * @returns {number[][]}
280
- */
281
- function detectCyclesFromNodes(graph, nodeSubset) {
282
- const { edges } = graph;
283
-
284
- let index = 0;
285
- /** @type {Map<number, number>} */
286
- const indices = new Map();
287
- /** @type {Map<number, number>} */
288
- const lowlink = new Map();
289
- /** @type {Set<number>} */
290
- const onStack = new Set();
291
- /** @type {number[]} */
292
- const stack = [];
293
- /** @type {number[][]} */
294
- const sccs = [];
295
-
296
- // Only process nodes in the subset.
297
- const subsetSet = new Set(nodeSubset);
298
-
299
- /**
300
- * Iterative Tarjan's SCC for a single root node.
301
- *
302
- * @param {number} root
303
- */
304
- function strongconnect(root) {
305
- // Explicit call-stack frames to avoid recursion limits.
306
- // Each frame: { iid, neighbourIter, parentIid }
307
- /** @type {Array<{ iid: number, neighbours: number[], ni: number }>} */
308
- const callStack = [];
309
-
310
- function visit(iid) {
311
- indices.set(iid, index);
312
- lowlink.set(iid, index);
313
- index++;
314
- stack.push(iid);
315
- onStack.add(iid);
316
-
317
- const neighbours = [...(edges.get(iid) ?? [])]
318
- .filter((n) => subsetSet.has(n))
319
- .sort((a, b) => a - b);
320
- callStack.push({ iid, neighbours, ni: 0 });
321
- }
322
-
323
- visit(root);
324
-
325
- while (callStack.length > 0) {
326
- const frame = callStack[callStack.length - 1];
327
- const { iid, neighbours } = frame;
328
-
329
- if (frame.ni < neighbours.length) {
330
- const w = neighbours[frame.ni++];
331
- if (!indices.has(w)) {
332
- visit(w);
333
- } else if (onStack.has(w)) {
334
- lowlink.set(iid, Math.min(lowlink.get(iid), indices.get(w)));
335
- }
336
- } else {
337
- // Pop frame.
338
- callStack.pop();
339
- if (callStack.length > 0) {
340
- const parent = callStack[callStack.length - 1];
341
- lowlink.set(
342
- parent.iid,
343
- Math.min(lowlink.get(parent.iid), lowlink.get(iid))
344
- );
345
- }
346
- // Check if this iid is an SCC root.
347
- if (lowlink.get(iid) === indices.get(iid)) {
348
- const scc = [];
349
- let w;
350
- do {
351
- w = stack.pop();
352
- onStack.delete(w);
353
- scc.push(w);
354
- } while (w !== iid);
355
- if (scc.length > 1) {
356
- sccs.push(scc.sort((a, b) => a - b));
357
- }
358
- }
359
- }
360
- }
361
- }
362
-
363
- for (const iid of nodeSubset) {
364
- if (!indices.has(iid)) {
365
- strongconnect(iid);
366
- }
367
- }
368
-
369
- // Sort by minimum iid in each SCC ascending.
370
- sccs.sort((a, b) => Math.min(...a) - Math.min(...b));
371
- return sccs;
372
- }
373
-
374
- // ---------------------------------------------------------------------------
375
- // nextReady
376
- // ---------------------------------------------------------------------------
377
-
378
- /**
379
- * Return the set of issues that are ready to be started next.
380
- *
381
- * An issue is considered ready when ALL of the following hold:
382
- * 1. It is NOT in `inFlight` (already being processed).
383
- * 2. It is NOT in `completed` (already done).
384
- * 3. Every iid in its `blockedBy` list is present in `completed`.
385
- * 4. It carries the label `"status:ready"`.
386
- *
387
- * The returned array is sorted by ascending iid.
388
- *
389
- * @param {DepGraph} graph - The dependency graph.
390
- * @param {Set<number>} inFlight - iids currently being processed.
391
- * @param {Set<number>} completed - iids that have finished successfully.
392
- * @returns {Issue[]}
393
- */
394
- export function nextReady(graph, inFlight, completed) {
395
- const safeInFlight = inFlight instanceof Set ? inFlight : new Set();
396
- const safeCompleted = completed instanceof Set ? completed : new Set();
397
-
398
- /** @type {Issue[]} */
399
- const ready = [];
400
-
401
- for (const [iid, issue] of graph.nodes) {
402
- if (safeInFlight.has(iid)) continue;
403
- if (safeCompleted.has(iid)) continue;
404
-
405
- // All blockers must be completed.
406
- const blockers = Array.isArray(issue.blockedBy) ? issue.blockedBy : [];
407
- const allBlockersCompleted = blockers.every((b) => safeCompleted.has(b));
408
- if (!allBlockersCompleted) continue;
409
-
410
- // Must carry status:ready label.
411
- if (!Array.isArray(issue.labels) || !issue.labels.includes('status:ready')) continue;
412
-
413
- ready.push(issue);
414
- }
415
-
416
- return ready.sort((a, b) => a.iid - b.iid);
417
- }
@@ -1,184 +0,0 @@
1
- // scripts/lib/autopilot/multi-killswitch.mjs
2
- //
3
- // 11th kill-switch (STALE_SUBAGENT_MIN) + cohort policy + concurrency cap for
4
- // autopilot --multi-story. Pure decision logic; no I/O, no shell.
5
- //
6
- // References:
7
- // - "Autopilot Phase D — Per-Story Worktree Pipelines" (#341; archived in the private Meta-Vault) (OPEN-2, OPEN-3, OPEN-5 decisions)
8
- // - scripts/lib/autopilot/kill-switches.mjs (existing 10 switches; STALL_TIMEOUT precedent)
9
- // - scripts/lib/resource-probe.mjs (snapshot shape)
10
-
11
- import { KILL_SWITCHES } from './kill-switches.mjs';
12
-
13
- // 11th kill-switch constant — additive to the existing 10 in KILL_SWITCHES
14
- /** @type {string} */
15
- export const STALE_SUBAGENT_MIN = 'stale-subagent-min';
16
-
17
- /**
18
- * Calculate concurrency cap per OPEN-2 decision from PRD §4.
19
- * Formula: min(staticFloor, max(1, floor(ram_free_gb / ramPerLoopGb) - reserveSlots))
20
- *
21
- * Defensive: null/undefined ram_free_gb → 1; memory_pressure_pct_free < 15 → cap 1.
22
- *
23
- * @param {{ ram_free_gb: number, memory_pressure_pct_free?: number, claude_processes_count?: number }} snapshot
24
- * @param {object} [opts]
25
- * @param {number} [opts.staticFloor=3] - hard cap regardless of resources
26
- * @param {number} [opts.ramPerLoopGb=4] - per-looper RAM budget
27
- * @param {number} [opts.reserveSlots=1] - sessions to reserve for system + coordinator
28
- * @returns {number} integer in [1, staticFloor]
29
- */
30
- export function calculateConcurrencyCap(snapshot, opts = {}) {
31
- const staticFloor = opts.staticFloor ?? 3;
32
- const ramPerLoopGb = opts.ramPerLoopGb ?? 4;
33
- const reserveSlots = opts.reserveSlots ?? 1;
34
-
35
- // macOS memory pressure degraded guard
36
- if (
37
- typeof snapshot?.memory_pressure_pct_free === 'number' &&
38
- snapshot.memory_pressure_pct_free < 15
39
- ) {
40
- return 1;
41
- }
42
-
43
- const ramFreeGb = snapshot?.ram_free_gb;
44
- if (ramFreeGb === null || ramFreeGb === undefined || typeof ramFreeGb !== 'number' || !Number.isFinite(ramFreeGb)) {
45
- return 1;
46
- }
47
-
48
- const computed = Math.floor(ramFreeGb / ramPerLoopGb) - reserveSlots;
49
- return Math.min(staticFloor, Math.max(1, computed));
50
- }
51
-
52
- /**
53
- * @typedef {object} LoopRegistration
54
- * @property {string} loopId
55
- * @property {number} pid
56
- * @property {string} parentRunId
57
- * @property {number} issueIid
58
- * @property {'queued'|'running'|'complete'|'failed'} status
59
- * @property {string|null} killSwitch
60
- * @property {number} spiralRecoveryCount
61
- * @property {number} startedAt - epoch ms
62
- * @property {number} lastActivityAt - epoch ms
63
- */
64
-
65
- /**
66
- * Evaluate cross-loop kill-switches (STALE_SUBAGENT_MIN only — per-loop
67
- * switches like SPIRAL are handled by postSessionKillSwitch upstream).
68
- * Returns the FIRST kill condition that fires, or null.
69
- *
70
- * @param {LoopRegistration[]} loops
71
- * @param {object} [opts]
72
- * @param {number} [opts.staleSubagentMinSeconds=600]
73
- * @param {() => number} [opts.nowMs=Date.now]
74
- * @returns {{ kill: string, detail: string, loopId?: string }|null}
75
- */
76
- export function evaluateMultiKillSwitches(loops, opts = {}) {
77
- if (!Array.isArray(loops) || loops.length === 0) return null;
78
-
79
- const staleThresholdSeconds = opts.staleSubagentMinSeconds ?? 600;
80
- const now = (opts.nowMs ?? Date.now)();
81
-
82
- for (const loop of loops) {
83
- if (!loop || loop.status !== 'running') continue;
84
-
85
- const lastActivity = loop.lastActivityAt;
86
- if (typeof lastActivity !== 'number' || !Number.isFinite(lastActivity)) continue;
87
-
88
- const idleMs = now - lastActivity;
89
- const idleSeconds = Math.floor(idleMs / 1000);
90
-
91
- if (idleMs > staleThresholdSeconds * 1000) {
92
- return {
93
- kill: STALE_SUBAGENT_MIN,
94
- detail: `loop ${loop.loopId} stale > ${idleSeconds}s (threshold ${staleThresholdSeconds}s)`,
95
- loopId: loop.loopId,
96
- };
97
- }
98
- }
99
-
100
- return null;
101
- }
102
-
103
- /**
104
- * Decide whether a sibling loop's failure should abort the cohort.
105
- * Per OPEN-5 hybrid policy:
106
- * - First SPIRAL AND target loop spiralRecoveryCount < 1 → retry
107
- * - Second SPIRAL OR target already retried → cohort-abort
108
- *
109
- * @param {LoopRegistration[]} loops
110
- * @param {string} failedLoopId
111
- * @returns {{ action: 'retry'|'cohort-abort', reason: string }}
112
- */
113
- export function decideCohortAction(loops, failedLoopId) {
114
- if (!Array.isArray(loops)) {
115
- return { action: 'cohort-abort', reason: 'no loop registry provided' };
116
- }
117
-
118
- const spiralCount = loops.filter(
119
- (l) => l && l.killSwitch === KILL_SWITCHES.SPIRAL,
120
- ).length;
121
-
122
- const targetLoop = loops.find((l) => l && l.loopId === failedLoopId);
123
- const targetSpiralRecoveries = targetLoop?.spiralRecoveryCount ?? 0;
124
-
125
- if (spiralCount <= 1 && targetSpiralRecoveries < 1) {
126
- return {
127
- action: 'retry',
128
- reason: 'first-strike spiral, recovering loop ' + failedLoopId,
129
- };
130
- }
131
-
132
- return {
133
- action: 'cohort-abort',
134
- reason: 'second-strike spiral or cohort threshold',
135
- };
136
- }
137
-
138
- /**
139
- * Check if the orchestrator stop condition is met (OPEN-3 layered policy).
140
- * Priority: (1) cohort-abort → (2) backlog-empty → (3) inactivity-timeout
141
- *
142
- * @param {object} state
143
- * @param {LoopRegistration[]} state.activeLoops
144
- * @param {object[]} state.readyBacklog
145
- * @param {number} state.lastCompletionAt
146
- * @param {object} [opts]
147
- * @param {number} [opts.inactivityTimeoutMs=300000]
148
- * @param {() => number} [opts.nowMs=Date.now]
149
- * @returns {{ stop: boolean, reason?: 'first-kill-switch'|'backlog-empty'|'inactivity-timeout' }}
150
- */
151
- export function shouldStopOrchestrator(state, opts = {}) {
152
- const inactivityTimeoutMs = opts.inactivityTimeoutMs ?? 300_000;
153
- const now = (opts.nowMs ?? Date.now)();
154
-
155
- const activeLoops = Array.isArray(state?.activeLoops) ? state.activeLoops : [];
156
- const readyBacklog = Array.isArray(state?.readyBacklog) ? state.readyBacklog : [];
157
-
158
- // Priority 1: cohort-abort triggered by spiral on an active loop
159
- for (const loop of activeLoops) {
160
- if (!loop || loop.killSwitch !== KILL_SWITCHES.SPIRAL) continue;
161
-
162
- const decision = decideCohortAction(activeLoops, loop.loopId);
163
- if (decision.action === 'cohort-abort') {
164
- return { stop: true, reason: 'first-kill-switch' };
165
- }
166
- }
167
-
168
- // Priority 2: all work drained
169
- if (activeLoops.length === 0 && readyBacklog.length === 0) {
170
- return { stop: true, reason: 'backlog-empty' };
171
- }
172
-
173
- // Priority 3: orchestrator inactivity timeout (only meaningful while loops are running)
174
- if (
175
- activeLoops.length > 0 &&
176
- typeof state?.lastCompletionAt === 'number' &&
177
- Number.isFinite(state.lastCompletionAt) &&
178
- now - state.lastCompletionAt > inactivityTimeoutMs
179
- ) {
180
- return { stop: true, reason: 'inactivity-timeout' };
181
- }
182
-
183
- return { stop: false };
184
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * webhook-url.mjs — Centralized webhook URL resolution for session-orchestrator.
3
- *
4
- * Replaces ad-hoc hardcoded personal-domain URL fallbacks with a single
5
- * resolution function that reads from environment variables and Session Config.
6
- * No personal-domain default exists — callers that need a URL must supply one
7
- * explicitly via env or config.
8
- *
9
- * Resolution order (env wins over config):
10
- * 1. Environment variable: SO_WEBHOOK_<KIND>_URL (e.g. SO_WEBHOOK_SLACK_URL)
11
- * 2. Session Config field: webhooks.<kind>.url
12
- * 3. Error: WebhookConfigError is thrown — no silent fallback to a personal domain.
13
- *
14
- * Supported kinds: 'slack' | 'discord' | 'generic' | 'gitlab-pipeline-status'
15
- *
16
- * Issue #228 — centralize webhook URL resolution + drop personal-domain default.
17
- */
18
-
19
- // ---------------------------------------------------------------------------
20
- // Named error
21
- // ---------------------------------------------------------------------------
22
-
23
- /**
24
- * Thrown by `resolveWebhookUrl` when no URL source (env or config) provides a
25
- * webhook URL for the requested kind. Callers should catch this and surface an
26
- * actionable message to the user.
27
- */
28
- export class WebhookConfigError extends Error {
29
- /**
30
- * @param {string} kind - The webhook kind that was requested.
31
- * @param {string} [message] - Optional override; defaults to a descriptive message.
32
- */
33
- constructor(kind, message) {
34
- super(
35
- message ??
36
- `No webhook URL configured for kind "${kind}". ` +
37
- `Set SO_WEBHOOK_${kind.toUpperCase().replace(/-/g, '_')}_URL ` +
38
- `or add webhooks.${kind}.url to Session Config.`,
39
- );
40
- this.name = 'WebhookConfigError';
41
- this.kind = kind;
42
- }
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // Supported kinds
47
- // ---------------------------------------------------------------------------
48
-
49
- /** @type {ReadonlySet<string>} */
50
- const SUPPORTED_KINDS = new Set([
51
- 'slack',
52
- 'discord',
53
- 'generic',
54
- 'gitlab-pipeline-status',
55
- ]);
56
-
57
- // ---------------------------------------------------------------------------
58
- // Public API
59
- // ---------------------------------------------------------------------------
60
-
61
- /**
62
- * Resolve a webhook URL for the given kind.
63
- *
64
- * Resolution order:
65
- * 1. `SO_WEBHOOK_<KIND>_URL` environment variable (e.g. `SO_WEBHOOK_SLACK_URL`)
66
- * — kind is upper-cased and hyphens become underscores.
67
- * 2. `config.webhooks.<kind>.url` — when a Session Config object is provided.
68
- * 3. Throws {@link WebhookConfigError} — no silent personal-domain fallback.
69
- *
70
- * @param {{ kind: string, config?: object }} options
71
- * - `kind` — One of the supported webhook kinds (see SUPPORTED_KINDS).
72
- * - `config` — Parsed Session Config object (optional). Must have a `webhooks`
73
- * sub-object if a config-based URL is desired.
74
- * @returns {string} The resolved webhook URL (guaranteed non-empty string).
75
- * @throws {WebhookConfigError} When kind is unsupported or no URL is found.
76
- * @throws {TypeError} When `kind` is not a string.
77
- */
78
- export function resolveWebhookUrl({ kind, config } = {}) {
79
- if (typeof kind !== 'string' || kind.length === 0) {
80
- throw new TypeError('resolveWebhookUrl: "kind" must be a non-empty string');
81
- }
82
-
83
- if (!SUPPORTED_KINDS.has(kind)) {
84
- throw new WebhookConfigError(
85
- kind,
86
- `Unsupported webhook kind "${kind}". Supported kinds: ${[...SUPPORTED_KINDS].join(', ')}.`,
87
- );
88
- }
89
-
90
- // 1. Environment variable: SO_WEBHOOK_<KIND>_URL
91
- const envKey = `SO_WEBHOOK_${kind.toUpperCase().replace(/-/g, '_')}_URL`;
92
- const envUrl = process.env[envKey];
93
- if (typeof envUrl === 'string' && envUrl.length > 0) {
94
- return envUrl;
95
- }
96
-
97
- // 2. Session Config: webhooks.<kind>.url
98
- const configUrl = config?.webhooks?.[kind]?.url;
99
- if (typeof configUrl === 'string' && configUrl.length > 0) {
100
- return configUrl;
101
- }
102
-
103
- // 3. No source provided a URL — throw rather than fall back to a personal domain.
104
- throw new WebhookConfigError(kind);
105
- }