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
@@ -29,7 +29,6 @@
29
29
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
30
30
  import { readFile } from 'node:fs/promises';
31
31
  import path from 'node:path';
32
- import os from 'node:os';
33
32
 
34
33
  import { emitEvent, sessionAttribution } from '../events.mjs';
35
34
  import { parseStateMd, parseMissionStatus } from '../state-md.mjs';
@@ -43,6 +42,7 @@ import { matchesModuloRedaction } from '../vault-mirror/process.mjs';
43
42
  import { readConfigFile, parseSessionConfig } from '../config.mjs';
44
43
  import { validatePathInsideProject } from '../path-utils.mjs';
45
44
  import { createSecretValueMasker } from '../secret-masker.mjs';
45
+ import { expandTilde } from '../common.mjs';
46
46
 
47
47
  /** Frontmatter sentinel that identifies generator-owned narrative files. */
48
48
  export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
@@ -63,23 +63,6 @@ const SECTION_TITLES = {
63
63
 
64
64
  // ── Raw section extraction ──────────────────────────────────────────────────────
65
65
 
66
- /**
67
- * Expand a leading `~` to the current user's home directory.
68
- *
69
- * NOTE: deferred shared-helper extraction. The same `expandHome` pattern lives in
70
- * other vault-status modules; W2 forbids introducing a shared new file, so this is
71
- * inlined here. Consolidate into a shared util in a follow-up wave.
72
- *
73
- * @param {string} p
74
- * @returns {string}
75
- */
76
- function expandHome(p) {
77
- if (typeof p !== 'string' || p.length === 0) return p;
78
- if (p === '~') return os.homedir();
79
- if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
80
- return p;
81
- }
82
-
83
66
  /**
84
67
  * Match an ATX markdown heading line. Returns `{ level, text }` or null.
85
68
  *
@@ -235,15 +218,27 @@ export function extractNarrative(stateMdContents) {
235
218
  * REFERENCE (byte-identical downstream), and `mask` passes non-strings through —
236
219
  * masking must never be the reason a mirror run dies.
237
220
  *
221
+ * Takes the ALREADY-BUILT masker (rather than building its own) so
222
+ * `runNarrativeMirror` can build it exactly ONCE per run and reuse it for both
223
+ * the actual masking below AND the `orchestrator.secret_masker.applied`
224
+ * telemetry (#1028) — the masker's `needleCount` is measured once, not
225
+ * potentially twice with process.env read between the two reads.
226
+ *
238
227
  * @param {{ waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }} narrative
239
- * @returns {{ waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }}
228
+ * @param {{ mask: (text: string) => string, needleCount: number }} masker
229
+ * @returns {{ narrative: { waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }, hits: number }}
240
230
  */
241
- function maskNarrative(narrative) {
242
- const { mask, needleCount } = createSecretValueMasker(process.env);
243
- if (needleCount === 0) return narrative;
231
+ function maskNarrative(narrative, masker) {
232
+ const { mask, needleCount } = masker;
233
+ if (needleCount === 0) return { narrative, hits: 0 };
244
234
 
235
+ let hits = 0;
245
236
  const walk = (value) => {
246
- if (typeof value === 'string') return mask(value);
237
+ if (typeof value === 'string') {
238
+ const masked = mask(value);
239
+ if (masked !== value) hits++;
240
+ return masked;
241
+ }
247
242
  if (Array.isArray(value)) return value.map(walk);
248
243
  if (value && typeof value === 'object') {
249
244
  const out = {};
@@ -253,7 +248,7 @@ function maskNarrative(narrative) {
253
248
  return value;
254
249
  };
255
250
 
256
- return walk(narrative);
251
+ return { narrative: walk(narrative), hits };
257
252
  }
258
253
 
259
254
  // ── Pure render ────────────────────────────────────────────────────────────────
@@ -421,6 +416,30 @@ function normalizeUpdated(content) {
421
416
  * between runs: run 1 (env set) → `skipped-noop`, run 2 (env absent) → `written`
422
417
  * with the raw value on disk.
423
418
  *
419
+ * WHY THERE IS NO `maskerWouldChange` RE-PROBE HERE (#1214). The two sibling
420
+ * sinks in `vault-mirror/process.mjs` (processLearning, processSession) guard
421
+ * all five of their `skipped-noop` returns with `maskerWouldChange(existing)`,
422
+ * because their five-field / date comparisons can report a match while the
423
+ * on-disk note still carries a raw needle. This sink compares the WHOLE
424
+ * document, which makes that residue unreachable: `matchesModuloRedaction`
425
+ * only fires when the EXISTING side carries a `[REDACTED]` marker, and every
426
+ * literal segment around such a marker must appear verbatim in the candidate —
427
+ * but the candidate comes through `maskNarrative` with the CURRENT masker, so
428
+ * it can never contain a value that masker would redact. A raw needle on disk
429
+ * therefore always lands in a literal segment that fails to match, and the run
430
+ * writes. Measured 2026-09-04 at HEAD cd785003 in both directions (no marker on
431
+ * disk; marker already on disk plus a second needle entering the env): both
432
+ * returned `written` with the raw value gone. The two tests named `(#1214)` in
433
+ * tests/lib/vault-status/narrative-mirror.test.mjs pin that invariant — an
434
+ * argument-order slip on the call below makes both return `skipped-noop` and
435
+ * republish the raw value, which is how they were verified to bite.
436
+ *
437
+ * NAMED CEILING: the invariant rests on `maskNarrative` walking EVERY string
438
+ * that reaches the render. `repo` is fed to `renderNarrative` outside that walk
439
+ * (it is a directory basename, not STATE.md content), so a future rendered
440
+ * field added outside the walk would reopen this. Revisit trigger: any new
441
+ * `renderNarrative` input that does not pass through `maskNarrative`.
442
+ *
424
443
  * `matchesModuloRedaction` is IMPORTED from `../vault-mirror/process.mjs`, not
425
444
  * re-derived here: `secret-masker.mjs`'s header states this compensation as a
426
445
  * contract binding on every consumer that diffs a written artifact against a fresh
@@ -681,6 +700,56 @@ async function emitNarrativeEvent({ repoRoot, action, path: outputPath, chars, e
681
700
  }
682
701
  }
683
702
 
703
+ /**
704
+ * Emit this channel's `orchestrator.secret_masker.applied` record (#1028
705
+ * residue 2). Until now the narrative mirror was the ONLY masker channel of
706
+ * the three (`vault-mirror`, `narrative-mirror`, `export-hw-learnings`) that
707
+ * never emitted this event — `maskNarrative` has masked unconditionally since
708
+ * #1025, but nothing told the ledger. A census grepping event NAMES for
709
+ * `narrative` therefore read as "this channel masks nothing", which was never
710
+ * true; it just never SAID so.
711
+ *
712
+ * Emitted UNCONDITIONALLY once per `mirrorNarrative` run whenever a `repoRoot`
713
+ * was resolvable — including every skip outcome, not only `written` — so
714
+ * `needle_count: 0` is a REAL measured "the env carries no secret-shaped
715
+ * value" rather than a stand-in for "the masker never ran" (same
716
+ * force-build-don't-default posture as `getMaskerStats()` in
717
+ * `scripts/lib/vault-mirror/process.mjs`). `records` is the literal `1`: one
718
+ * `mirrorNarrative` run always masks (or attempts to mask) exactly one
719
+ * STATE.md, unlike the CLI channels that fan out over many JSONL lines.
720
+ *
721
+ * Same field set as the `vault-mirror` channel's own masker emit
722
+ * (`channel`, `needle_count`, `records`, `hits`, `dry_run`), PLUS
723
+ * `session_id`/`semantic_session_id` via `sessionAttribution` — matching this
724
+ * module's own `emitNarrativeEvent` above rather than the CLI's 2-arg
725
+ * `emitEvent` call, because `mirrorNarrative` (unlike the CLI) already always
726
+ * carries a `repoRoot` to attribute against.
727
+ *
728
+ * Best-effort: never throws, never alters the mirror result.
729
+ *
730
+ * @param {{ repoRoot?: string, needleCount: number, hits: number, dryRun: boolean }} opts
731
+ * @returns {Promise<void>}
732
+ */
733
+ async function emitMaskerEvent({ repoRoot, needleCount, hits, dryRun }) {
734
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) return;
735
+ try {
736
+ await emitEvent(
737
+ 'orchestrator.secret_masker.applied',
738
+ {
739
+ channel: 'narrative-mirror',
740
+ needle_count: needleCount,
741
+ records: 1,
742
+ hits,
743
+ dry_run: dryRun,
744
+ ...sessionAttribution(repoRoot),
745
+ },
746
+ { repoRoot },
747
+ );
748
+ } catch {
749
+ /* Best-effort telemetry — see emitNarrativeEvent above for why. */
750
+ }
751
+ }
752
+
684
753
  // ── Convenience orchestration ────────────────────────────────────────────────────
685
754
 
686
755
  /**
@@ -740,42 +809,62 @@ export async function mirrorNarrative(opts) {
740
809
  chars: outcome.chars,
741
810
  });
742
811
 
812
+ // #1028 residue 2: the masker-telemetry sibling of the narrative event
813
+ // above, on every non-throwing outcome (including skips) — see
814
+ // emitMaskerEvent for why this fires unconditionally.
815
+ await emitMaskerEvent({
816
+ repoRoot: opts?.repoRoot,
817
+ needleCount: outcome.needleCount,
818
+ hits: outcome.hits,
819
+ dryRun: outcome.dryRun,
820
+ });
821
+
743
822
  return outcome.result;
744
823
  }
745
824
 
746
825
  /**
747
826
  * The mirror itself — every early return of {@link mirrorNarrative} lives here.
748
827
  *
749
- * Split out so that exactly ONE emit site covers EVERY outcome: a future early
750
- * return added inside this function is telemetered by construction, whereas
751
- * hand-placing an emit beside each of the seven `return`s makes "forgot the new
752
- * one" the default failure. The `chars` companion travels beside the result
828
+ * Split out so that the two emit sites in `mirrorNarrative` (the narrative
829
+ * event AND, since #1028, the masker event) each cover EVERY outcome from
830
+ * ONE call: a future early return added inside this function is telemetered
831
+ * by construction, whereas hand-placing an emit beside each of the seven
832
+ * `return`s makes "forgot the new one" the default failure. The `chars` companion travels beside the result
753
833
  * rather than inside it because the returned object is a PUBLIC shape that
754
834
  * callers (and tests) compare with `toEqual` — adding a key there would be an
755
835
  * observable contract change for a purely internal measurement.
756
836
  *
757
837
  * @param {Parameters<typeof mirrorNarrative>[0]} opts
758
- * @returns {Promise<{ result: { action: string, path?: string }, chars?: number }>}
838
+ * @returns {Promise<{ result: { action: string, path?: string }, chars?: number, needleCount: number, hits: number, dryRun: boolean }>}
759
839
  */
760
840
  async function runNarrativeMirror(opts) {
761
841
  const { repoRoot, repo, now = new Date(), dryRun = false, fs: injectedFs, hostPaths } = opts;
762
842
 
763
843
  if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
764
- return { result: { action: 'skipped-vault-disabled' } };
844
+ return { result: { action: 'skipped-vault-disabled' }, needleCount: 0, hits: 0, dryRun };
765
845
  }
766
846
 
847
+ // #1028: the masker is built ONCE per run, right here — before every
848
+ // remaining early-return below — so `mirrorNarrative`'s
849
+ // `orchestrator.secret_masker.applied` emit always carries a REAL measured
850
+ // `needle_count` on every outcome (including a skip), rather than a
851
+ // defaulted 0 that would be indistinguishable from "the masker never ran".
852
+ // Same "force-build, never default" posture as `getMaskerStats()` in
853
+ // `scripts/lib/vault-mirror/process.mjs`.
854
+ const masker = createSecretValueMasker(process.env);
855
+
767
856
  // Read Session Config (CLAUDE.md / AGENTS.md) and resolve vault settings.
768
857
  let config;
769
858
  try {
770
859
  const configText = await readConfigFile(repoRoot);
771
860
  config = parseSessionConfig(configText, { hostPaths });
772
861
  } catch {
773
- return { result: { action: 'skipped-vault-disabled' } };
862
+ return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
774
863
  }
775
864
 
776
865
  const vaultIntegration = config?.['vault-integration'];
777
866
  if (!vaultIntegration || vaultIntegration.enabled !== true) {
778
- return { result: { action: 'skipped-vault-disabled' } };
867
+ return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
779
868
  }
780
869
 
781
870
  // Defense-in-depth: when the caller omits (or passes an empty) `repo`, derive
@@ -792,10 +881,10 @@ async function runNarrativeMirror(opts) {
792
881
 
793
882
  const rawVaultDir = vaultIntegration['vault-dir'];
794
883
  if (!rawVaultDir || typeof rawVaultDir !== 'string') {
795
- return { result: { action: 'skipped-vault-disabled' } };
884
+ return { result: { action: 'skipped-vault-disabled' }, needleCount: masker.needleCount, hits: 0, dryRun };
796
885
  }
797
886
 
798
- const vaultDir = path.resolve(expandHome(rawVaultDir));
887
+ const vaultDir = path.resolve(expandTilde(rawVaultDir));
799
888
  const candidateSlug = subjectToSlug(repoName) || 'unknown';
800
889
  // Loose-match against existing 01-projects/ folders before minting a new
801
890
  // slug (issue #829 Finding 3) — see resolveLooseSlug for the ambiguity
@@ -816,7 +905,7 @@ async function runNarrativeMirror(opts) {
816
905
  canonicalizeRoot: true,
817
906
  });
818
907
  if (!inside.ok) {
819
- return { result: { action: 'skipped-invalid-path', path: outputPath } };
908
+ return { result: { action: 'skipped-invalid-path', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
820
909
  }
821
910
 
822
911
  // Read STATE.md (best-effort; absent STATE.md → nothing to mirror).
@@ -825,13 +914,13 @@ async function runNarrativeMirror(opts) {
825
914
  try {
826
915
  stateContents = await readFile(stateMdPath, 'utf8');
827
916
  } catch {
828
- return { result: { action: 'skipped-no-statemd', path: outputPath } };
917
+ return { result: { action: 'skipped-no-statemd', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
829
918
  }
830
919
 
831
920
  // #1025: the ONE masking site for the narrative mirror — after the frontmatter
832
921
  // parse (so `[REDACTED]` can never break it) and before the render, the
833
922
  // idempotency comparison and the write. See maskNarrative above.
834
- const narrative = maskNarrative(extractNarrative(stateContents));
923
+ const { narrative, hits } = maskNarrative(extractNarrative(stateContents), masker);
835
924
  const content = renderNarrative({ repo: repoName, narrative, now });
836
925
 
837
926
  // `chars` measures the document THIS call rendered — so it is present on
@@ -840,5 +929,8 @@ async function runNarrativeMirror(opts) {
840
929
  return {
841
930
  result: writeNarrative({ outputPath, content, dryRun, fs: injectedFs }),
842
931
  chars: content.length,
932
+ needleCount: masker.needleCount,
933
+ hits,
934
+ dryRun,
843
935
  };
844
936
  }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * dispatch-common.mjs — shared base for the two foreign-dispatch adapters:
3
+ * `foreign-dispatch.mjs` (Cursor channel, `cursor-agent`) and
4
+ * `remote-dispatch.mjs` (remote-host channel, `offload`). Two adapters, one
5
+ * base (#1204).
6
+ *
7
+ * These six symbols moved here because each one is an enforcement point or a
8
+ * budget constant that must NOT diverge between the two channels: the
9
+ * `never_foreign` role lock, the run-id path-segment validator, the
10
+ * SIGTERM→SIGKILL child runner, and the two dispatch-budget constants. Before
11
+ * this module existed, `remote-dispatch.mjs` imported all five from
12
+ * `foreign-dispatch.mjs` directly — a Cursor-specific file acting as a shared
13
+ * base for a channel that has nothing to do with Cursor. Both adapters now
14
+ * import from here instead, so neither exports something the other must reach
15
+ * through.
16
+ *
17
+ * Revisit-Trigger: if a THIRD dispatch channel ever needs a different budget
18
+ * default or a different never-foreign list, split the constant per-channel
19
+ * at that point rather than overriding it at the call site — a call-site
20
+ * override would silently diverge from the one enforcement point this module
21
+ * exists to be.
22
+ */
23
+
24
+ /**
25
+ * Roles that may NEVER run on a foreign model. Copied verbatim from
26
+ * account-switch `tools/routing/routing.yaml` § never_foreign (read 2026-08-25).
27
+ * Keep in sync by hand — the YAML lives in a different repo and is parsed there
28
+ * by awk, not by a shared library.
29
+ * @type {readonly string[]}
30
+ */
31
+ export const NEVER_FOREIGN_ROLES = Object.freeze([
32
+ 'impl-core',
33
+ 'security-review',
34
+ 'migration',
35
+ 'release',
36
+ 'secrets',
37
+ 'incident',
38
+ 'refactor-crosscut',
39
+ ]);
40
+
41
+ /** Default wall-clock budget for one foreign dispatch, in seconds.
42
+ * This is a FLOOR, not a suggestion: measured 2026-08-23, `cursor-grok-4.6-high`
43
+ * ran 2 of 3 hard-test tasks past a 540 s cap (recorded as DNF). Lowering this
44
+ * manufactures timeouts and reads as model failure. */
45
+ export const DEFAULT_TIMEOUT_SEC = 900;
46
+
47
+ /**
48
+ * Grace period between SIGTERM and SIGKILL, in ms.
49
+ *
50
+ * `child.kill('SIGTERM')` is a REQUEST: a child that installs a handler and
51
+ * ignores it never emits `close`, and this module's only resolve paths are
52
+ * `close` and `error` — so without escalation a wedged `cursor-agent` hangs
53
+ * `dispatchForeign` forever and takes the whole wave with it. SIGKILL is not
54
+ * catchable, so the escalation always terminates.
55
+ *
56
+ * Named ceiling (BV-004): 10 s is enough for a cooperative child to flush its
57
+ * stream-json tail and exit. Revisit if a well-behaved `cursor-agent` is ever
58
+ * measured needing longer than that to shut down — raise the constant, never
59
+ * drop the escalation.
60
+ */
61
+ export const DEFAULT_KILL_GRACE_MS = 10_000;
62
+
63
+ /** A runId names a directory and a log file. Anything outside this alphabet —
64
+ * or the two relative-path literals the alphabet happens to admit — can escape
65
+ * the parent it is joined to. */
66
+ const SAFE_RUN_ID = /^[A-Za-z0-9._-]+$/;
67
+
68
+ /**
69
+ * @param {unknown} runId
70
+ * @returns {boolean} true when the id is safe to use as a path segment.
71
+ */
72
+ export function isSafeRunId(runId) {
73
+ const s = String(runId ?? '');
74
+ if (s === '.' || s === '..') return false; // admitted by SAFE_RUN_ID, still an escape
75
+ return SAFE_RUN_ID.test(s);
76
+ }
77
+
78
+ /**
79
+ * @param {string} role
80
+ * @returns {boolean} true when the role is locked to Claude.
81
+ */
82
+ export function isNeverForeignRole(role) {
83
+ return NEVER_FOREIGN_ROLES.includes(String(role || '').trim());
84
+ }
85
+
86
+ /**
87
+ * Run the child process, capturing stdout+stderr merged, with a hand-rolled
88
+ * SIGTERM timeout that ESCALATES to SIGKILL. `spawnFn` is the DI seam so tests
89
+ * never touch the real `cursor-agent` binary.
90
+ *
91
+ * The escalation is the difference between a bounded dispatch and a hung wave:
92
+ * this function's only resolve paths are the child's `close` and `error`
93
+ * events, so a child that ignores SIGTERM never lets the promise settle. See
94
+ * {@link DEFAULT_KILL_GRACE_MS}.
95
+ *
96
+ * @returns {Promise<{capture: string, exitCode: number|null, signal: string|null,
97
+ * timerFired: boolean, killSignals: string[], errorCode: string|null}>}
98
+ */
99
+ export function runChild(cmd, args, options, { spawnFn, timeoutMs, killGraceMs }) {
100
+ return new Promise((resolve) => {
101
+ /** @type {string[]} */
102
+ const killSignals = [];
103
+ let child;
104
+ try {
105
+ child = spawnFn(cmd, args, options);
106
+ } catch (err) {
107
+ resolve({
108
+ capture: `spawn failed: ${err && err.message}`,
109
+ exitCode: null,
110
+ signal: null,
111
+ timerFired: false,
112
+ killSignals,
113
+ errorCode: (err && err.code) || null,
114
+ });
115
+ return;
116
+ }
117
+
118
+ let capture = '';
119
+ let timerFired = false;
120
+ let settled = false;
121
+ let errorCode = null;
122
+ /** @type {ReturnType<typeof setTimeout>|null} */
123
+ let graceTimer = null;
124
+
125
+ const onChunk = (chunk) => {
126
+ capture += chunk.toString();
127
+ };
128
+ child.stdout?.on('data', onChunk);
129
+ child.stderr?.on('data', onChunk);
130
+
131
+ const send = (signal) => {
132
+ killSignals.push(signal);
133
+ try {
134
+ child.kill(signal);
135
+ } catch {
136
+ /* already gone */
137
+ }
138
+ };
139
+
140
+ const timer = setTimeout(() => {
141
+ timerFired = true;
142
+ send('SIGTERM');
143
+ // SIGTERM is a request; SIGKILL is not. A child that installs a handler
144
+ // and declines to exit would otherwise hang this promise forever.
145
+ graceTimer = setTimeout(() => send('SIGKILL'), killGraceMs);
146
+ graceTimer.unref?.();
147
+ }, timeoutMs);
148
+
149
+ const finish = (exitCode, signal) => {
150
+ if (settled) return;
151
+ settled = true;
152
+ clearTimeout(timer);
153
+ if (graceTimer) clearTimeout(graceTimer);
154
+ resolve({ capture, exitCode, signal, timerFired, killSignals, errorCode });
155
+ };
156
+
157
+ child.on('error', (err) => {
158
+ errorCode = (err && err.code) || null;
159
+ capture += `\nchild error: ${err && err.message}`;
160
+ finish(null, null);
161
+ });
162
+ child.on('close', (code, signal) => finish(code, signal ?? null));
163
+ });
164
+ }
@@ -54,45 +54,13 @@ import path from 'node:path';
54
54
 
55
55
  import { emitEvent, sessionAttribution } from '../events.mjs';
56
56
  import { isPathInside } from '../path-utils.mjs';
57
-
58
- /**
59
- * Roles that may NEVER run on a foreign model. Copied verbatim from
60
- * account-switch `tools/routing/routing.yaml` § never_foreign (read 2026-08-25).
61
- * Keep in sync by hand — the YAML lives in a different repo and is parsed there
62
- * by awk, not by a shared library.
63
- * @type {readonly string[]}
64
- */
65
- export const NEVER_FOREIGN_ROLES = Object.freeze([
66
- 'impl-core',
67
- 'security-review',
68
- 'migration',
69
- 'release',
70
- 'secrets',
71
- 'incident',
72
- 'refactor-crosscut',
73
- ]);
74
-
75
- /** Default wall-clock budget for one foreign dispatch, in seconds.
76
- * This is a FLOOR, not a suggestion: measured 2026-08-23, `cursor-grok-4.6-high`
77
- * ran 2 of 3 hard-test tasks past a 540 s cap (recorded as DNF). Lowering this
78
- * manufactures timeouts and reads as model failure. */
79
- export const DEFAULT_TIMEOUT_SEC = 900;
80
-
81
- /**
82
- * Grace period between SIGTERM and SIGKILL, in ms.
83
- *
84
- * `child.kill('SIGTERM')` is a REQUEST: a child that installs a handler and
85
- * ignores it never emits `close`, and this module's only resolve paths are
86
- * `close` and `error` — so without escalation a wedged `cursor-agent` hangs
87
- * `dispatchForeign` forever and takes the whole wave with it. SIGKILL is not
88
- * catchable, so the escalation always terminates.
89
- *
90
- * Named ceiling (BV-004): 10 s is enough for a cooperative child to flush its
91
- * stream-json tail and exit. Revisit if a well-behaved `cursor-agent` is ever
92
- * measured needing longer than that to shut down — raise the constant, never
93
- * drop the escalation.
94
- */
95
- export const DEFAULT_KILL_GRACE_MS = 10_000;
57
+ import {
58
+ DEFAULT_KILL_GRACE_MS,
59
+ DEFAULT_TIMEOUT_SEC,
60
+ isNeverForeignRole,
61
+ isSafeRunId,
62
+ runChild,
63
+ } from './dispatch-common.mjs';
96
64
 
97
65
  /**
98
66
  * argv prefix for every foreign dispatch, ahead of `--model`/`--output-format`.
@@ -156,29 +124,6 @@ export const FOREIGN_ENV_ALLOWLIST = Object.freeze([
156
124
  * directory named this way. */
157
125
  export const FOREIGN_TMP_PREFIX = 'so-foreign';
158
126
 
159
- /** A runId names a directory and a log file. Anything outside this alphabet —
160
- * or the two relative-path literals the alphabet happens to admit — can escape
161
- * the parent it is joined to. */
162
- const SAFE_RUN_ID = /^[A-Za-z0-9._-]+$/;
163
-
164
- /**
165
- * @param {unknown} runId
166
- * @returns {boolean} true when the id is safe to use as a path segment.
167
- */
168
- function isSafeRunId(runId) {
169
- const s = String(runId ?? '');
170
- if (s === '.' || s === '..') return false; // admitted by SAFE_RUN_ID, still an escape
171
- return SAFE_RUN_ID.test(s);
172
- }
173
-
174
- /**
175
- * @param {string} role
176
- * @returns {boolean} true when the role is locked to Claude.
177
- */
178
- export function isNeverForeignRole(role) {
179
- return NEVER_FOREIGN_ROLES.includes(String(role || '').trim());
180
- }
181
-
182
127
  /**
183
128
  * Build the allowlisted child environment.
184
129
  * @param {Record<string, string|undefined>} [source]
@@ -317,86 +262,6 @@ export function parseCursorStream(raw) {
317
262
  */
318
263
  export const MEASUREMENT_EXCLUDES = Object.freeze(['node_modules']);
319
264
 
320
- /**
321
- * Run the child process, capturing stdout+stderr merged, with a hand-rolled
322
- * SIGTERM timeout that ESCALATES to SIGKILL. `spawnFn` is the DI seam so tests
323
- * never touch the real `cursor-agent` binary.
324
- *
325
- * The escalation is the difference between a bounded dispatch and a hung wave:
326
- * this function's only resolve paths are the child's `close` and `error`
327
- * events, so a child that ignores SIGTERM never lets the promise settle. See
328
- * {@link DEFAULT_KILL_GRACE_MS}.
329
- *
330
- * @returns {Promise<{capture: string, exitCode: number|null, signal: string|null,
331
- * timerFired: boolean, killSignals: string[], errorCode: string|null}>}
332
- */
333
- function runChild(cmd, args, options, { spawnFn, timeoutMs, killGraceMs }) {
334
- return new Promise((resolve) => {
335
- /** @type {string[]} */
336
- const killSignals = [];
337
- let child;
338
- try {
339
- child = spawnFn(cmd, args, options);
340
- } catch (err) {
341
- resolve({
342
- capture: `spawn failed: ${err && err.message}`,
343
- exitCode: null,
344
- signal: null,
345
- timerFired: false,
346
- killSignals,
347
- errorCode: (err && err.code) || null,
348
- });
349
- return;
350
- }
351
-
352
- let capture = '';
353
- let timerFired = false;
354
- let settled = false;
355
- let errorCode = null;
356
- /** @type {ReturnType<typeof setTimeout>|null} */
357
- let graceTimer = null;
358
-
359
- const onChunk = (chunk) => {
360
- capture += chunk.toString();
361
- };
362
- child.stdout?.on('data', onChunk);
363
- child.stderr?.on('data', onChunk);
364
-
365
- const send = (signal) => {
366
- killSignals.push(signal);
367
- try {
368
- child.kill(signal);
369
- } catch {
370
- /* already gone */
371
- }
372
- };
373
-
374
- const timer = setTimeout(() => {
375
- timerFired = true;
376
- send('SIGTERM');
377
- // SIGTERM is a request; SIGKILL is not. A child that installs a handler
378
- // and declines to exit would otherwise hang this promise forever.
379
- graceTimer = setTimeout(() => send('SIGKILL'), killGraceMs);
380
- graceTimer.unref?.();
381
- }, timeoutMs);
382
-
383
- const finish = (exitCode, signal) => {
384
- if (settled) return;
385
- settled = true;
386
- clearTimeout(timer);
387
- if (graceTimer) clearTimeout(graceTimer);
388
- resolve({ capture, exitCode, signal, timerFired, killSignals, errorCode });
389
- };
390
-
391
- child.on('error', (err) => {
392
- errorCode = (err && err.code) || null;
393
- capture += `\nchild error: ${err && err.message}`;
394
- finish(null, null);
395
- });
396
- child.on('close', (code, signal) => finish(code, signal ?? null));
397
- });
398
- }
399
-
400
265
  /** Read `git` output as text, returning '' on failure (a detached worktree with
401
266
  * no commits still answers, but a broken invocation must not throw here). */
402
267
  function gitText(args, execFn) {