session-orchestrator 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (484) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +6 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +7 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +8 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +21 -0
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +7 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +7 -2
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +7 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +7 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +7 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +8 -3
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +22 -0
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +7 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +20 -0
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +6 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +6 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +8 -2
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +21 -0
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +6 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +22 -0
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +6 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +6 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +7 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +21 -0
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +7 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  83. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  84. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  85. package/.codex-plugin/skills/close/SKILL.md +3 -3
  86. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
  87. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  88. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  89. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  90. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  91. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  92. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  93. package/.codex-plugin/skills/go/SKILL.md +3 -3
  94. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  95. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  96. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  97. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
  98. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  99. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  100. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  101. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  102. package/.codex-plugin/skills/release/SKILL.md +3 -3
  103. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  104. package/.codex-plugin/skills/session/SKILL.md +1 -1
  105. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  106. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  107. package/.codex-plugin/skills/test/SKILL.md +3 -3
  108. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  109. package/.cursor/commands/autopilot.md +4 -4
  110. package/.cursor/commands/bootstrap.md +5 -4
  111. package/.cursor/commands/brainstorm.md +5 -4
  112. package/.cursor/commands/close.md +4 -3
  113. package/.cursor/commands/debug.md +4 -4
  114. package/.cursor/commands/discovery.md +4 -4
  115. package/.cursor/commands/dispatcher.md +4 -4
  116. package/.cursor/commands/eli5.md +4 -4
  117. package/.cursor/commands/eval.md +4 -4
  118. package/.cursor/commands/evolve.md +4 -4
  119. package/.cursor/commands/go.md +4 -3
  120. package/.cursor/commands/grill.md +4 -4
  121. package/.cursor/commands/harness-audit.md +3 -3
  122. package/.cursor/commands/memory-cleanup.md +4 -4
  123. package/.cursor/commands/persona-panel.md +4 -4
  124. package/.cursor/commands/plan.md +5 -4
  125. package/.cursor/commands/portfolio.md +3 -3
  126. package/.cursor/commands/reconcile.md +4 -4
  127. package/.cursor/commands/release.md +4 -3
  128. package/.cursor/commands/repo-audit.md +4 -4
  129. package/.cursor/commands/session.md +1 -1
  130. package/.cursor/commands/spinout.md +4 -4
  131. package/.cursor/commands/sunset-review.md +4 -4
  132. package/.cursor/commands/test.md +3 -3
  133. package/.cursor/commands/ux-grill.md +4 -4
  134. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  135. package/.cursor/rules/010-session-workflow.mdc +2 -2
  136. package/.cursor/rules/050-plan.mdc +1 -1
  137. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  138. package/.cursor/skills/close/SKILL.md +13 -0
  139. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  140. package/.cursor/skills/debug/SKILL.md +0 -1
  141. package/.cursor/skills/discovery/SKILL.md +0 -1
  142. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  143. package/.cursor/skills/eli5/SKILL.md +0 -1
  144. package/.cursor/skills/eval/SKILL.md +1 -1
  145. package/.cursor/skills/evolve/SKILL.md +0 -1
  146. package/.cursor/skills/go/SKILL.md +13 -0
  147. package/.cursor/skills/grill/SKILL.md +0 -1
  148. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  149. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  150. package/.cursor/skills/portfolio/SKILL.md +12 -0
  151. package/.cursor/skills/release/SKILL.md +13 -0
  152. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  153. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  154. package/.cursor/skills/test/SKILL.md +12 -0
  155. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  156. package/.cursor-plugin/plugin.json +1 -1
  157. package/.orchestrator/policy/blocked-commands.json +13 -4
  158. package/AGENTS.md +3 -2
  159. package/CHANGELOG.md +197 -0
  160. package/README.md +11 -9
  161. package/SECURITY.md +12 -0
  162. package/agents/dialectic-deriver.md +13 -10
  163. package/agents/eval-judge.md +67 -45
  164. package/agents/skill-applied-judge.md +34 -19
  165. package/commands/session.md +17 -3
  166. package/docs/baseline.md +12 -6
  167. package/docs/ci-setup.md +53 -0
  168. package/docs/codex-setup.md +15 -3
  169. package/docs/components.md +13 -6
  170. package/docs/events-schema.md +59 -9
  171. package/docs/install.md +16 -0
  172. package/docs/persona-panel.md +1 -1
  173. package/docs/pi-setup.md +1 -1
  174. package/docs/rule-authoring.md +135 -14
  175. package/docs/scope-collision-guard.md +2 -0
  176. package/docs/session-config-reference.md +106 -11
  177. package/docs/session-config-template.md +31 -2
  178. package/docs/telemetry.md +2 -0
  179. package/hooks/_lib/hook-import-set.json +125 -8
  180. package/hooks/_lib/subagent-paths.mjs +15 -0
  181. package/hooks/_lib/subagent-transcript.mjs +582 -31
  182. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  183. package/hooks/config-protection.mjs +11 -3
  184. package/hooks/cwd-change-restore.mjs +11 -3
  185. package/hooks/enforce-commands.mjs +70 -23
  186. package/hooks/enforce-scope.mjs +143 -33
  187. package/hooks/hooks-codex.json +1 -1
  188. package/hooks/hooks.json +1 -1
  189. package/hooks/loop-guard.mjs +11 -3
  190. package/hooks/on-session-end.mjs +72 -25
  191. package/hooks/on-session-start.mjs +48 -11
  192. package/hooks/on-stop.mjs +211 -23
  193. package/hooks/operator-steer.mjs +11 -3
  194. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  195. package/hooks/post-bash-write-verify.mjs +6 -2
  196. package/hooks/post-edit-import-probe.mjs +17 -9
  197. package/hooks/post-edit-validate.mjs +13 -5
  198. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  199. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  200. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  201. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  202. package/hooks/pre-auq-clarity.mjs +18 -2
  203. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  204. package/hooks/pre-bash-issue-budget.mjs +119 -28
  205. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  206. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  207. package/hooks/pre-bash-staging-fence.mjs +335 -31
  208. package/hooks/pre-bash-templates-first.mjs +19 -14
  209. package/hooks/pre-task-scope-disjoint.mjs +385 -5
  210. package/hooks/skill-invocation-telemetry.mjs +2 -1
  211. package/hooks/subagent-telemetry.mjs +15 -19
  212. package/hooks/wave-scope-commit-guard.mjs +197 -100
  213. package/monitors/monitors.json +1 -1
  214. package/output-styles/wave-summary.md +1 -1
  215. package/package.json +2 -1
  216. package/pi/prompts/autopilot.md +3 -3
  217. package/pi/prompts/bootstrap.md +3 -3
  218. package/pi/prompts/brainstorm.md +3 -3
  219. package/pi/prompts/close.md +2 -2
  220. package/pi/prompts/debug.md +3 -3
  221. package/pi/prompts/discovery.md +3 -3
  222. package/pi/prompts/dispatcher.md +3 -3
  223. package/pi/prompts/eli5.md +3 -3
  224. package/pi/prompts/eval.md +3 -3
  225. package/pi/prompts/evolve.md +3 -3
  226. package/pi/prompts/go.md +2 -2
  227. package/pi/prompts/grill.md +3 -3
  228. package/pi/prompts/harness-audit.md +2 -3
  229. package/pi/prompts/memory-cleanup.md +3 -3
  230. package/pi/prompts/persona-panel.md +3 -3
  231. package/pi/prompts/plan.md +3 -3
  232. package/pi/prompts/portfolio.md +2 -2
  233. package/pi/prompts/reconcile.md +3 -3
  234. package/pi/prompts/release.md +3 -3
  235. package/pi/prompts/repo-audit.md +3 -4
  236. package/pi/prompts/session.md +2 -2
  237. package/pi/prompts/spinout.md +3 -3
  238. package/pi/prompts/sunset-review.md +3 -3
  239. package/pi/prompts/templates-ack.md +1 -1
  240. package/pi/prompts/test.md +3 -3
  241. package/pi/prompts/ux-grill.md +3 -3
  242. package/rules/README.md +1 -1
  243. package/rules/opt-in-domain/prompt-caching.md +1 -1
  244. package/rules/opt-in-stack/backend-data.md +1 -1
  245. package/rules/opt-in-stack/backend.md +3 -3
  246. package/rules/opt-in-stack/frontend.md +1 -1
  247. package/rules/opt-in-stack/security-web.md +3 -3
  248. package/rules/opt-in-stack/swift.md +1 -1
  249. package/scripts/archive-closed-prds.mjs +2 -2
  250. package/scripts/auq-audit.mjs +2 -3
  251. package/scripts/autopilot.mjs +23 -2
  252. package/scripts/backfill-abandoned-sessions.mjs +171 -15
  253. package/scripts/backfill-evidence-digest.mjs +2 -1
  254. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  255. package/scripts/check-package-manager.mjs +2 -2
  256. package/scripts/check-sessions-integrity.mjs +300 -0
  257. package/scripts/ci/assert-vitest-green.mjs +2 -1
  258. package/scripts/dialectic-deriver.mjs +50 -13
  259. package/scripts/emit-session.mjs +77 -32
  260. package/scripts/eval-session.mjs +65 -3
  261. package/scripts/export-hw-learnings.mjs +2 -1
  262. package/scripts/express-path.mjs +1 -1
  263. package/scripts/gc-stale-worktrees.mjs +2 -1
  264. package/scripts/generate-agents-skills.mjs +102 -29
  265. package/scripts/generate-codex-skills.mjs +48 -4
  266. package/scripts/generate-cursor-adapter.mjs +220 -11
  267. package/scripts/generate-hook-import-set.mjs +12 -27
  268. package/scripts/generate-pi-prompts.mjs +183 -13
  269. package/scripts/github-protection-audit.mjs +2 -3
  270. package/scripts/lib/agent-frontmatter.mjs +23 -1
  271. package/scripts/lib/agent-status.mjs +2 -31
  272. package/scripts/lib/auq/clarity.mjs +10 -2
  273. package/scripts/lib/auq/parse.mjs +12 -31
  274. package/scripts/lib/auq/schema.mjs +56 -41
  275. package/scripts/lib/auto-dialectic.mjs +304 -15
  276. package/scripts/lib/autopilot/flags.mjs +12 -1
  277. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  278. package/scripts/lib/autopilot/loop.mjs +14 -1
  279. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  280. package/scripts/lib/ci-status-banner.mjs +376 -16
  281. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  282. package/scripts/lib/command-blocker.mjs +408 -33
  283. package/scripts/lib/config/dialectic.mjs +12 -3
  284. package/scripts/lib/config/drift-check.mjs +19 -0
  285. package/scripts/lib/config/gate.mjs +74 -0
  286. package/scripts/lib/config/reaper.mjs +162 -0
  287. package/scripts/lib/config.mjs +14 -0
  288. package/scripts/lib/convergence-monitor.mjs +76 -13
  289. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  290. package/scripts/lib/description-surface.mjs +2 -5
  291. package/scripts/lib/dispatcher/cli.mjs +2 -1
  292. package/scripts/lib/ecosystem-health.mjs +11 -0
  293. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  294. package/scripts/lib/eval/engine.mjs +421 -53
  295. package/scripts/lib/eval/judge.mjs +463 -40
  296. package/scripts/lib/eval/schema.mjs +10 -1
  297. package/scripts/lib/events-rotation.mjs +221 -25
  298. package/scripts/lib/events-schema.mjs +114 -0
  299. package/scripts/lib/events.mjs +524 -5
  300. package/scripts/lib/fetch-baseline.mjs +3 -8
  301. package/scripts/lib/frontmatter-guard.mjs +21 -10
  302. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  303. package/scripts/lib/gates/gate-full.mjs +28 -3
  304. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  305. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  306. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  307. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  308. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  309. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  310. package/scripts/lib/instruction-budget-guard.mjs +332 -50
  311. package/scripts/lib/io.mjs +42 -8
  312. package/scripts/lib/is-main-module.mjs +82 -0
  313. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  314. package/scripts/lib/js-mask.mjs +197 -0
  315. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  316. package/scripts/lib/locks/index.mjs +32 -25
  317. package/scripts/lib/maintenance-due-banner.mjs +122 -91
  318. package/scripts/lib/orphan-reaper.mjs +1588 -0
  319. package/scripts/lib/peer-cards/merger.mjs +48 -10
  320. package/scripts/lib/peer-cards/reader.mjs +78 -2
  321. package/scripts/lib/peer-discovery.mjs +2 -5
  322. package/scripts/lib/playwright-driver/runner.mjs +2 -1
  323. package/scripts/lib/process-group.mjs +899 -0
  324. package/scripts/lib/quality-gate.mjs +107 -28
  325. package/scripts/lib/reconcile/backlog.mjs +368 -0
  326. package/scripts/lib/reconcile/engine.mjs +55 -188
  327. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
  328. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  329. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  330. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  331. package/scripts/lib/rule-loader.mjs +41 -12
  332. package/scripts/lib/rules-sync.mjs +2 -5
  333. package/scripts/lib/scope-echo.mjs +429 -7
  334. package/scripts/lib/scope-gate.mjs +605 -1
  335. package/scripts/lib/session-close-backfill.mjs +91 -12
  336. package/scripts/lib/session-id.mjs +9 -20
  337. package/scripts/lib/session-invocation.mjs +20 -0
  338. package/scripts/lib/session-schema/constants.mjs +30 -2
  339. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  340. package/scripts/lib/session-schema.mjs +8 -3
  341. package/scripts/lib/session-start-probes.mjs +95 -10
  342. package/scripts/lib/sessions-canonical.mjs +23 -0
  343. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  344. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  345. package/scripts/lib/skill-evidence-window.mjs +891 -0
  346. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  347. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  348. package/scripts/lib/skill-judge.mjs +45 -3
  349. package/scripts/lib/state-md.mjs +84 -3
  350. package/scripts/lib/sunset/walker.mjs +31 -4
  351. package/scripts/lib/tail-window.mjs +56 -0
  352. package/scripts/lib/telemetry/schema.mjs +30 -0
  353. package/scripts/lib/telemetry/sync.mjs +61 -6
  354. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  355. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  356. package/scripts/lib/tests-src-ratio.mjs +2 -6
  357. package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
  358. package/scripts/lib/user-invocable-skills.mjs +205 -0
  359. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  360. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  361. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  362. package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
  363. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  364. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  365. package/scripts/lib/validate/check-entry-guard.mjs +329 -0
  366. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  367. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  368. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  369. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  370. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  371. package/scripts/lib/validate/check-rules.mjs +7 -5
  372. package/scripts/lib/validate/check-skill-links.mjs +35 -6
  373. package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
  374. package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
  375. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  376. package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
  377. package/scripts/lib/validate/check-unwired-features.mjs +220 -33
  378. package/scripts/lib/validate/check-validator-registration.mjs +36 -12
  379. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  380. package/scripts/lib/validate/confidential-names.mjs +10 -0
  381. package/scripts/lib/validate-vendored-rules.mjs +39 -12
  382. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  383. package/scripts/lib/vault-mirror/process.mjs +10 -3
  384. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  385. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  386. package/scripts/lib/vault-yaml.mjs +118 -0
  387. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  388. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  389. package/scripts/lock-reaper.mjs +2 -1
  390. package/scripts/materialize-wave-scope.mjs +87 -4
  391. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  392. package/scripts/migrate-vault-paths.mjs +2 -3
  393. package/scripts/release-session-lock.mjs +305 -0
  394. package/scripts/release.mjs +109 -39
  395. package/scripts/relocate-vault-corpus.mjs +2 -3
  396. package/scripts/repair-invalid-sessions.mjs +2 -2
  397. package/scripts/resolve-session-invocation.mjs +59 -0
  398. package/scripts/run-quality-gate.mjs +156 -17
  399. package/scripts/session-shape.mjs +2 -2
  400. package/scripts/site-numbers.mjs +35 -11
  401. package/scripts/sweep-expired-rules.mjs +227 -0
  402. package/scripts/validate-plugin.mjs +21 -0
  403. package/scripts/validate-wave-scope.mjs +32 -105
  404. package/scripts/vault-consolidate.mjs +2 -2
  405. package/scripts/vault-mirror.mjs +11 -4
  406. package/scripts/wave-scope-binding.mjs +2 -3
  407. package/skills/_shared/bootstrap-gate.md +1 -1
  408. package/skills/_shared/monitor-patterns.md +1 -1
  409. package/skills/_shared/platform-tools.md +23 -11
  410. package/skills/_shared/research-evidence.md +53 -0
  411. package/skills/_shared/state-ownership.md +3 -0
  412. package/skills/autopilot/SKILL.md +80 -11
  413. package/skills/bootstrap/SKILL.md +51 -1
  414. package/skills/brainstorm/SKILL.md +16 -0
  415. package/skills/claude-md-drift-check/SKILL.md +1 -1
  416. package/skills/claude-md-drift-check/checker.mjs +49 -11
  417. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  418. package/skills/convergence-monitoring/README.md +8 -1
  419. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  420. package/skills/convergence-monitoring/SKILL.md +15 -6
  421. package/skills/debug/SKILL.md +10 -0
  422. package/skills/discovery/SKILL.md +24 -1
  423. package/skills/discovery/probes-session.md +2 -2
  424. package/skills/dispatcher/SKILL.md +38 -7
  425. package/skills/eli5/SKILL.md +11 -0
  426. package/skills/eval/SKILL.md +52 -23
  427. package/skills/eval/rubric-v1.md +1 -0
  428. package/skills/eval/rubric-v2.md +457 -0
  429. package/skills/evolve/SKILL.md +9 -2
  430. package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
  431. package/skills/gitlab-ops/SKILL.md +3 -2
  432. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  433. package/skills/grill/SKILL.md +19 -0
  434. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  435. package/skills/hook-development/SKILL.md +46 -41
  436. package/skills/memory-cleanup/SKILL.md +7 -0
  437. package/skills/npm-publish/SKILL.md +2 -2
  438. package/skills/persona-panel/SKILL.md +56 -1
  439. package/skills/persona-panel/persona-format.md +1 -1
  440. package/skills/plan/SKILL.md +28 -1
  441. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  442. package/skills/reconcile/SKILL.md +21 -0
  443. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  444. package/skills/repo-audit/SKILL.md +7 -0
  445. package/skills/session-end/SKILL.md +13 -16
  446. package/skills/session-end/discovery-scan.md +1 -1
  447. package/skills/session-end/phase-3-6-tail.md +55 -9
  448. package/skills/session-end/plan-verification.md +2 -2
  449. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  450. package/skills/session-end/session-metrics-write.md +10 -0
  451. package/skills/session-plan/SKILL.md +18 -6
  452. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  453. package/skills/session-start/SKILL.md +5 -4
  454. package/skills/session-start/phase-8-5-express-path.md +6 -6
  455. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  456. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  457. package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
  458. package/skills/spinout/SKILL.md +12 -1
  459. package/skills/sunset-review/SKILL.md +13 -0
  460. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  461. package/skills/ux-grill/SKILL.md +20 -2
  462. package/skills/wave-executor/SKILL.md +14 -7
  463. package/skills/wave-executor/circuit-breaker.md +2 -0
  464. package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
  465. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  466. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  467. package/commands/autopilot.md +0 -80
  468. package/commands/bootstrap.md +0 -56
  469. package/commands/brainstorm.md +0 -48
  470. package/commands/debug.md +0 -36
  471. package/commands/discovery.md +0 -32
  472. package/commands/dispatcher.md +0 -59
  473. package/commands/eli5.md +0 -33
  474. package/commands/eval.md +0 -28
  475. package/commands/evolve.md +0 -10
  476. package/commands/grill.md +0 -45
  477. package/commands/memory-cleanup.md +0 -26
  478. package/commands/persona-panel.md +0 -121
  479. package/commands/plan.md +0 -15
  480. package/commands/reconcile.md +0 -23
  481. package/commands/repo-audit.md +0 -24
  482. package/commands/spinout.md +0 -15
  483. package/commands/sunset-review.md +0 -27
  484. package/commands/ux-grill.md +0 -51
@@ -10,6 +10,31 @@ import { runCheck } from './gate-helpers.mjs';
10
10
  const typecheckCmd = process.env.TYPECHECK_CMD;
11
11
  const testCmd = process.env.TEST_CMD;
12
12
 
13
+ /**
14
+ * Per-command wall-clock ceiling, published by `scripts/run-quality-gate.mjs`
15
+ * as `GATE_TIMEOUT_MS` (#1425 A3 / #1432). It carries the ALREADY-RESOLVED
16
+ * value — operator override `SO_GATE_TIMEOUT_MS` > Session Config
17
+ * `gate.timeout-path-b-ms` > 900 000 — so this script only has to read it.
18
+ *
19
+ * Absent or non-numeric (this gate invoked directly, not through the wrapper)
20
+ * → the option is OMITTED, and `runCheck` falls back to `resolveGateTimeoutMs()`
21
+ * exactly as before. An empty object is deliberate: passing `timeoutMs:
22
+ * undefined` would NOT trigger that fallback in every spread order.
23
+ */
24
+ const CHECK_OPTS = (() => {
25
+ const raw = Number((process.env.GATE_TIMEOUT_MS || '').trim());
26
+ const opts = Number.isFinite(raw) && raw > 0 ? { timeoutMs: raw } : {};
27
+ // GATE_LEDGER_ROOT (#1425 A4, W5 fix-pass): the wrapper publishes the ledger
28
+ // root it resolved (--ledger-root > repo root) so the gate-process register
29
+ // is written where the WRAPPER decided, not where this sub-script happens to
30
+ // run. Without it every runCheck() here defaulted to process.cwd(), and a test
31
+ // that spawned this script from the checkout wrote real lines into the live
32
+ // .orchestrator/runtime/gate-processes.jsonl — the reaper's kill population.
33
+ const ledgerRoot = (process.env.GATE_LEDGER_ROOT || "").trim();
34
+ if (ledgerRoot && ledgerRoot.startsWith("/")) opts.repoRoot = ledgerRoot;
35
+ return opts;
36
+ })();
37
+
13
38
  if (!typecheckCmd) {
14
39
  process.stderr.write('TYPECHECK_CMD must be set\n');
15
40
  process.exit(1);
@@ -23,11 +48,11 @@ let tcResult = { status: 'skip', output: '' };
23
48
  let testResult = { status: 'skip', output: '' };
24
49
 
25
50
  if (typecheckCmd !== 'skip') {
26
- tcResult = await runCheck(typecheckCmd);
51
+ tcResult = await runCheck(typecheckCmd, CHECK_OPTS);
27
52
  }
28
53
 
29
54
  if (testCmd !== 'skip') {
30
- testResult = await runCheck(testCmd);
55
+ testResult = await runCheck(testCmd, CHECK_OPTS);
31
56
  }
32
57
 
33
58
  const result = {
@@ -18,6 +18,31 @@ const testCmd = process.env.TEST_CMD;
18
18
  const lintCmd = process.env.LINT_CMD;
19
19
  const sessionStartRef = process.env.SESSION_START_REF ?? '';
20
20
 
21
+ /**
22
+ * Per-command wall-clock ceiling, published by `scripts/run-quality-gate.mjs`
23
+ * as `GATE_TIMEOUT_MS` (#1425 A3 / #1432). It carries the ALREADY-RESOLVED
24
+ * value — operator override `SO_GATE_TIMEOUT_MS` > Session Config
25
+ * `gate.timeout-path-b-ms` > 900 000 — so this script only has to read it.
26
+ *
27
+ * Absent or non-numeric (this gate invoked directly, not through the wrapper)
28
+ * → the option is OMITTED, and `runCheck` falls back to `resolveGateTimeoutMs()`
29
+ * exactly as before. An empty object is deliberate: passing `timeoutMs:
30
+ * undefined` would NOT trigger that fallback in every spread order.
31
+ */
32
+ const CHECK_OPTS = (() => {
33
+ const raw = Number((process.env.GATE_TIMEOUT_MS || '').trim());
34
+ const opts = Number.isFinite(raw) && raw > 0 ? { timeoutMs: raw } : {};
35
+ // GATE_LEDGER_ROOT (#1425 A4, W5 fix-pass): the wrapper publishes the ledger
36
+ // root it resolved (--ledger-root > repo root) so the gate-process register
37
+ // is written where the WRAPPER decided, not where this sub-script happens to
38
+ // run. Without it every runCheck() here defaulted to process.cwd(), and a test
39
+ // that spawned this script from the checkout wrote real lines into the live
40
+ // .orchestrator/runtime/gate-processes.jsonl — the reaper's kill population.
41
+ const ledgerRoot = (process.env.GATE_LEDGER_ROOT || "").trim();
42
+ if (ledgerRoot && ledgerRoot.startsWith("/")) opts.repoRoot = ledgerRoot;
43
+ return opts;
44
+ })();
45
+
21
46
  if (!typecheckCmd) {
22
47
  process.stderr.write('TYPECHECK_CMD must be set\n');
23
48
  process.exit(1);
@@ -34,7 +59,7 @@ if (!lintCmd) {
34
59
  const startTime = Date.now();
35
60
 
36
61
  // --- Typecheck ---
37
- const tcResult = runCheck(typecheckCmd);
62
+ const tcResult = await runCheck(typecheckCmd, CHECK_OPTS);
38
63
  const tcErrorCount =
39
64
  tcResult.status === 'fail'
40
65
  ? extractCount(tcResult.fullOutput ?? tcResult.output, /error TS\d+/)
@@ -44,7 +69,7 @@ const tcErrorCount =
44
69
  // NOTE: `testCounts`, NOT `failed` — a local `failed` is already bound near
45
70
  // the bottom of this file and drives `process.exit(failed ? 2 : 0)`.
46
71
  // Shadowing it would corrupt the gate's exit code.
47
- const testResult = runCheck(testCmd);
72
+ const testResult = await runCheck(testCmd, CHECK_OPTS);
48
73
  const testCounts =
49
74
  testResult.status !== 'skip'
50
75
  ? extractTestCounts(testResult.fullOutput ?? testResult.output)
@@ -98,7 +123,7 @@ const fileFields = testCounts.files
98
123
  : {};
99
124
 
100
125
  // --- Lint ---
101
- const lintResult = runCheck(lintCmd);
126
+ const lintResult = await runCheck(lintCmd, CHECK_OPTS);
102
127
  const lintWarnings =
103
128
  lintResult.status !== 'skip'
104
129
  ? extractCount(lintResult.fullOutput ?? lintResult.output, /warning/i)
@@ -8,10 +8,176 @@
8
8
 
9
9
  import { execSync } from 'node:child_process';
10
10
  import { readFileSync } from 'node:fs';
11
+
12
+ import {
13
+ DEFAULT_GATE_TIMEOUT_MS,
14
+ buildCommandSignature,
15
+ spawnInGroup,
16
+ } from '../process-group.mjs';
11
17
  import { detectStubCommand } from './echo-stub-detect.mjs';
12
18
 
13
19
  const RUN_CHECK_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
14
20
 
21
+ /** Lines of captured output kept in the human-facing `output` tail. */
22
+ const OUTPUT_TAIL_LINES = 5;
23
+
24
+ /**
25
+ * Name of the per-invocation override for the gate wall-clock ceiling.
26
+ *
27
+ * Read at CALL time, never at module load, so a test (or a caller that sets it
28
+ * for one child) is not defeated by import order.
29
+ *
30
+ * Named ceiling (BV-004): this is an ENV var, and an env var is inherited by
31
+ * every descendant — the exact shape that made `SO_GATE_LEDGER_ROOT` reach
32
+ * every vitest worker on 2026-09-06 (`scripts/run-quality-gate.mjs`, the
33
+ * `--ledger-root` block). It is acceptable here only because the value is a
34
+ * CEILING every descendant should honour anyway. A future Session-Config
35
+ * wiring of `gate.timeout-path-b-ms` should prefer the explicit `timeoutMs`
36
+ * option below — which reaches exactly one call — over exporting this name.
37
+ */
38
+ export const GATE_TIMEOUT_ENV = 'SO_GATE_TIMEOUT_MS';
39
+
40
+ /**
41
+ * The wall-clock ceiling one gate command is allowed, in ms.
42
+ *
43
+ * Precedence: `process.env.SO_GATE_TIMEOUT_MS` when set to a finite positive
44
+ * number, else {@link DEFAULT_GATE_TIMEOUT_MS} (900_000 — the PRD's
45
+ * `gate.timeout-path-b-ms`, deliberately the same 15 min the synchronous
46
+ * path A already had, so both gate paths are allowed exactly as long).
47
+ *
48
+ * A non-numeric or non-positive value is IGNORED rather than honoured: a typo
49
+ * that parsed as 0 would disable the cap, which is the failure this whole
50
+ * change exists to remove.
51
+ *
52
+ * @returns {number} positive milliseconds
53
+ */
54
+ export function resolveGateTimeoutMs() {
55
+ const raw = (process.env[GATE_TIMEOUT_ENV] || '').trim();
56
+ if (!raw) return DEFAULT_GATE_TIMEOUT_MS;
57
+ const parsed = Number(raw);
58
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_GATE_TIMEOUT_MS;
59
+ return parsed;
60
+ }
61
+
62
+ /**
63
+ * Last {@link OUTPUT_TAIL_LINES} lines of a captured text, trimmed.
64
+ *
65
+ * @param {string} text
66
+ * @returns {string}
67
+ */
68
+ function tailOf(text) {
69
+ return String(text ?? '').split('\n').slice(-OUTPUT_TAIL_LINES).join('\n').trim();
70
+ }
71
+
72
+ /**
73
+ * The one line a timed-out gate command MUST carry.
74
+ *
75
+ * A timeout is a FAILURE with a name, never a silent `fail`: without this line
76
+ * a killed command is indistinguishable in the envelope from a command that
77
+ * exited non-zero on its own, and the operator has nothing to act on. It names
78
+ * the ceiling that fired, the process GROUP that was signalled, the ladder that
79
+ * ran, and — the part an exit code can never carry — anything that SURVIVED
80
+ * SIGKILL (PRD B6: a sent signal proves nothing).
81
+ *
82
+ * @param {number} timeoutMs
83
+ * @param {{pgid: number, killSignals: string[], survivors: number[]}} run
84
+ * @returns {string}
85
+ */
86
+ function timeoutLine(timeoutMs, run) {
87
+ const ladder = (run.killSignals ?? []).join('\u2192') || 'no signal sent';
88
+ return `gate: TIMEOUT after ${timeoutMs} ms \u2014 process group ${run.pgid} ${ladder}, `
89
+ + `survivors: [${(run.survivors ?? []).join(', ')}]`;
90
+ }
91
+
92
+ /**
93
+ * The stdout envelope `scripts/run-quality-gate.mjs` publishes when the GATE
94
+ * SUB-SCRIPT itself was killed on the wall-clock ceiling.
95
+ *
96
+ * Lives here, beside the other envelope helpers, because the CLI that consumes
97
+ * it is a top-level script with no exports — a pure function there would be
98
+ * untestable without executing the CLI.
99
+ *
100
+ * Deliberately carries no `test`/`typecheck`/`lint` object: a killed gate
101
+ * measured nothing, and `suiteCountsFromGateStdout` must return `null` for it
102
+ * (absent is not zero). `error: 'gate-timeout'` is the machine-readable
103
+ * discriminator; the exit code is 124, the same value `spawnInGroup` reports
104
+ * and the same one coreutils `timeout(1)` uses.
105
+ *
106
+ * @param {object} args
107
+ * @param {string} args.variant The `--variant` value the run was started with.
108
+ * @param {number} args.timeoutMs Ceiling that fired.
109
+ * @param {{pgid: number, durationMs: number, killSignals: string[], survivors: number[]}} args.run
110
+ * The {@link spawnInGroup} result.
111
+ * @returns {{variant: string, error: 'gate-timeout', timeout_ms: number, duration_ms: number,
112
+ * pgid: number, kill_signals: string[], survivors: number[]}}
113
+ */
114
+ export function gateTimeoutEnvelope({ variant, timeoutMs, run }) {
115
+ return {
116
+ variant,
117
+ error: 'gate-timeout',
118
+ timeout_ms: timeoutMs,
119
+ duration_ms: run?.durationMs ?? 0,
120
+ pgid: run?.pgid ?? -1,
121
+ kill_signals: run?.killSignals ?? [],
122
+ survivors: run?.survivors ?? [],
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Decide WHAT a finished gate sub-script run publishes — stdout, stderr,
128
+ * exit code and operator warnings — without performing any of the writes.
129
+ *
130
+ * ## Why this is a pure function and not four `process.*.write` calls
131
+ *
132
+ * The timeout branch in `scripts/run-quality-gate.mjs` was unreachable by any
133
+ * test: reaching it required a REAL gate sub-script to exceed
134
+ * `resolveGateTimeoutMs() + GATE_OUTER_TIMEOUT_RESERVE_MS` (15 min + 60 s, a
135
+ * hard constant with no injection seam), so the four decisions it makes —
136
+ * suppress the partial capture on stdout, re-publish it on stderr, emit ONE
137
+ * complete `gate-timeout` envelope, warn about survivors — were pinned by
138
+ * nothing. The CLI now decides here and only WRITES there, so each decision is
139
+ * testable against a synthetic {@link spawnInGroup} result.
140
+ *
141
+ * ## The one-document contract on stdout
142
+ *
143
+ * A killed child never wrote its envelope, so its capture is at best a partial
144
+ * JSON document. Publishing that hands every stdout consumer a parse error
145
+ * where a named failure belongs; publishing BOTH the partial text and an
146
+ * envelope breaks the "one JSON document" contract. Hence: capture → stderr,
147
+ * envelope → stdout, and `stdout` carries EXACTLY the envelope line.
148
+ *
149
+ * @param {object} args
150
+ * @param {{fullOutput?: string, exitCode?: number, timedOut?: boolean, pgid?: number,
151
+ * durationMs?: number, killSignals?: string[], survivors?: number[]}} args.result
152
+ * The {@link spawnInGroup} result for the gate sub-script.
153
+ * @param {string} args.variant The `--variant` value the run was started with.
154
+ * @param {number} args.timeoutMs The OUTER ceiling that applied to the sub-script.
155
+ * @returns {{stdout: string, stderr: string, exitCode: number, warnings: string[]}}
156
+ * `stdout`/`stderr` are written verbatim (empty string = write nothing);
157
+ * `warnings` go through the caller's `warn()`; `exitCode` is the gate's own,
158
+ * which `spawnInGroup` reports as 124 on the timeout path.
159
+ */
160
+ export function publishGateOutcome({ result, variant, timeoutMs }) {
161
+ const capture = String(result?.fullOutput ?? '');
162
+ const exitCode = result?.exitCode ?? 0;
163
+
164
+ if (!result?.timedOut) {
165
+ return { stdout: capture, stderr: '', exitCode, warnings: [] };
166
+ }
167
+
168
+ const survivors = result.survivors ?? [];
169
+ return {
170
+ stdout: `${JSON.stringify(gateTimeoutEnvelope({ variant, timeoutMs, run: result }))}\n`,
171
+ stderr: capture.trim()
172
+ ? `\n\u2500\u2500\u2500\u2500 gate TIMED OUT \u2014 captured output before the kill \u2500\u2500\u2500\u2500\n${capture}\n\u2500\u2500\u2500\u2500 end \u2500\u2500\u2500\u2500\n`
173
+ : '',
174
+ exitCode,
175
+ warnings: survivors.length > 0
176
+ ? [`gate process group ${result.pgid} left survivors after SIGKILL: ${survivors.join(', ')}`]
177
+ : [],
178
+ };
179
+ }
180
+
15
181
  // ---------------------------------------------------------------------------
16
182
  // Internal pattern helpers
17
183
  // ---------------------------------------------------------------------------
@@ -39,7 +205,8 @@ function isTestFile(filePath) {
39
205
  // ---------------------------------------------------------------------------
40
206
 
41
207
  /**
42
- * Execute a shell command and return a structured result.
208
+ * Execute a shell command as the LEADER OF ITS OWN PROCESS GROUP, under a
209
+ * wall-clock ceiling, and return a structured result.
43
210
  *
44
211
  * `output` is a bounded TAIL for humans. `fullOutput` is the complete captured
45
212
  * text and is what every COUNT parse must read.
@@ -52,10 +219,47 @@ function isTestFile(filePath) {
52
219
  * "the runner never produced results" and hides WHICH test failed. An hour was
53
220
  * spent chasing that phantom before the real cause (one red test) was found.
54
221
  *
222
+ * ## Why a process group, and why a timeout (Epic #1425 A3)
223
+ *
224
+ * This used to be `execSync(cmd, { maxBuffer })` — no timeout at all, and the
225
+ * shell as the only signalled process. On 2026-09-20 four `tsgo --noEmit`
226
+ * grandchildren of gate runs outlived their parents at PPID 1 with up to 8.0 GB
227
+ * RSS each and took the host to 13 % free memory. {@link spawnInGroup} spawns
228
+ * `detached`, so `process.kill(-pgid, …)` reaches every descendant, and it runs
229
+ * the SIGTERM→grace→SIGKILL ladder with a read-back verification.
230
+ *
231
+ * ## What changed for callers
232
+ *
233
+ * 1. It is ASYNC. Every call site must `await`.
234
+ * 2. `fullOutput` now interleaves stdout AND stderr on the PASS path too
235
+ * (`execSync` discarded stderr when the command succeeded). A runner that
236
+ * prints its summary to stderr is therefore no longer invisible to
237
+ * {@link extractTestCounts}.
238
+ * 3. Three fields are added — `timedOut`, `killSignals`, `survivors` — and they
239
+ * are present ONLY when a process actually ran. A skipped or stubbed command
240
+ * spawned nothing, so it carries no `timedOut: false`: absent is not a
241
+ * measured false, the same contract `counts` and `files` already keep.
242
+ *
243
+ * A timeout is a REPORTED failure: `status: 'fail'`, `exitCode: 124`, and a
244
+ * `gate: TIMEOUT after …` line appended to `output`/`fullOutput` naming the
245
+ * ceiling, the group, the signal ladder and any survivor. It is never a silent
246
+ * `fail`.
247
+ *
55
248
  * @param {string} cmd - Shell command to run, or `"skip"` / empty to skip.
56
- * @returns {{ status: 'pass'|'fail'|'skip', output: string, fullOutput: string, exitCode: number, stubbed?: { kind: 'echo'|'noop' } }}
249
+ * @param {object} [opts] - Forwarded verbatim to {@link spawnInGroup}, and it
250
+ * OVERRIDES the defaults below (`timeoutMs`, `maxOutputBytes`, `repoRoot`,
251
+ * `commandSignature`). This is also the seam tests inject `spawnFn` /
252
+ * `killFn` / `isAliveFn` through.
253
+ * @param {number|null} [opts.timeoutMs] - Wall-clock ceiling; defaults to
254
+ * {@link resolveGateTimeoutMs} (`SO_GATE_TIMEOUT_MS` or 900_000). `null`
255
+ * disables the clock — the byte cap still applies.
256
+ * @param {string} [opts.repoRoot] - Root whose gate-process ledger the spawn is
257
+ * recorded in; defaults to `process.cwd()`, which is the tree under test.
258
+ * @returns {Promise<{ status: 'pass'|'fail'|'skip', output: string, fullOutput: string,
259
+ * exitCode: number, timedOut?: boolean, killSignals?: string[], survivors?: number[],
260
+ * stubbed?: { kind: 'echo'|'noop' } }>}
57
261
  */
58
- export function runCheck(cmd) {
262
+ export async function runCheck(cmd, opts = {}) {
59
263
  if (!cmd || cmd === 'skip') {
60
264
  return { status: 'skip', output: '', fullOutput: '', exitCode: 0 };
61
265
  }
@@ -65,26 +269,44 @@ export function runCheck(cmd) {
65
269
  return { status: 'pass', output: `(stubbed: ${stub.kind})`, fullOutput: `(stubbed: ${stub.kind})`, exitCode: 0, stubbed: { kind: stub.kind } };
66
270
  }
67
271
 
68
- try {
69
- const raw = execSync(cmd, {
70
- encoding: 'utf8',
71
- stdio: ['pipe', 'pipe', 'pipe'],
72
- maxBuffer: RUN_CHECK_MAX_BUFFER_BYTES,
73
- });
74
- const output = raw.split('\n').slice(-5).join('\n').trim();
75
- return { status: 'pass', output, fullOutput: raw, exitCode: 0 };
76
- } catch (err) {
77
- const exitCode = typeof err.status === 'number' ? err.status : 1;
78
-
79
- // Exit code 127 means command not found — treat as skip.
80
- if (exitCode === 127) {
81
- return { status: 'skip', output: 'command not found', fullOutput: '', exitCode };
82
- }
272
+ const timeoutMs = opts.timeoutMs === undefined ? resolveGateTimeoutMs() : opts.timeoutMs;
273
+ const run = await spawnInGroup(cmd, {
274
+ maxOutputBytes: RUN_CHECK_MAX_BUFFER_BYTES,
275
+ repoRoot: process.cwd(),
276
+ commandSignature: buildCommandSignature(cmd),
277
+ ...opts,
278
+ timeoutMs,
279
+ });
280
+
281
+ // Reported in every returned shape below, so a consumer never has to ask a
282
+ // second question to learn whether the group is actually gone.
283
+ const groupFields = { killSignals: run.killSignals, survivors: run.survivors };
284
+
285
+ if (run.timedOut) {
286
+ const fullOutput = `${run.fullOutput}\n${timeoutLine(timeoutMs, run)}\n`;
287
+ return {
288
+ status: 'fail',
289
+ output: tailOf(fullOutput),
290
+ fullOutput,
291
+ exitCode: run.exitCode,
292
+ timedOut: true,
293
+ ...groupFields,
294
+ };
295
+ }
83
296
 
84
- const combined = [err.stdout ?? '', err.stderr ?? ''].join('\n');
85
- const output = combined.split('\n').slice(-5).join('\n').trim();
86
- return { status: 'fail', output, fullOutput: combined, exitCode };
297
+ // Exit code 127 means command not found — treat as skip.
298
+ if (run.exitCode === 127) {
299
+ return { status: 'skip', output: 'command not found', fullOutput: '', exitCode: 127, timedOut: false, ...groupFields };
87
300
  }
301
+
302
+ return {
303
+ status: run.exitCode === 0 ? 'pass' : 'fail',
304
+ output: tailOf(run.fullOutput),
305
+ fullOutput: run.fullOutput,
306
+ exitCode: run.exitCode,
307
+ timedOut: false,
308
+ ...groupFields,
309
+ };
88
310
  }
89
311
 
90
312
  /**
@@ -13,6 +13,31 @@ const testCmd = process.env.TEST_CMD;
13
13
  const files = process.env.FILES ?? '';
14
14
  const sessionStartRef = process.env.SESSION_START_REF ?? '';
15
15
 
16
+ /**
17
+ * Per-command wall-clock ceiling, published by `scripts/run-quality-gate.mjs`
18
+ * as `GATE_TIMEOUT_MS` (#1425 A3 / #1432). It carries the ALREADY-RESOLVED
19
+ * value — operator override `SO_GATE_TIMEOUT_MS` > Session Config
20
+ * `gate.timeout-path-b-ms` > 900 000 — so this script only has to read it.
21
+ *
22
+ * Absent or non-numeric (this gate invoked directly, not through the wrapper)
23
+ * → the option is OMITTED, and `runCheck` falls back to `resolveGateTimeoutMs()`
24
+ * exactly as before. An empty object is deliberate: passing `timeoutMs:
25
+ * undefined` would NOT trigger that fallback in every spread order.
26
+ */
27
+ const CHECK_OPTS = (() => {
28
+ const raw = Number((process.env.GATE_TIMEOUT_MS || '').trim());
29
+ const opts = Number.isFinite(raw) && raw > 0 ? { timeoutMs: raw } : {};
30
+ // GATE_LEDGER_ROOT (#1425 A4, W5 fix-pass): the wrapper publishes the ledger
31
+ // root it resolved (--ledger-root > repo root) so the gate-process register
32
+ // is written where the WRAPPER decided, not where this sub-script happens to
33
+ // run. Without it every runCheck() here defaulted to process.cwd(), and a test
34
+ // that spawned this script from the checkout wrote real lines into the live
35
+ // .orchestrator/runtime/gate-processes.jsonl — the reaper's kill population.
36
+ const ledgerRoot = (process.env.GATE_LEDGER_ROOT || "").trim();
37
+ if (ledgerRoot && ledgerRoot.startsWith("/")) opts.repoRoot = ledgerRoot;
38
+ return opts;
39
+ })();
40
+
16
41
  if (!typecheckCmd) {
17
42
  process.stderr.write('TYPECHECK_CMD must be set\n');
18
43
  process.exit(1);
@@ -28,7 +53,7 @@ let testStatus;
28
53
  let errors = [];
29
54
 
30
55
  // --- typecheck (always runs unless cmd is "skip") ---
31
- const tcResult = await runCheck(typecheckCmd);
56
+ const tcResult = await runCheck(typecheckCmd, CHECK_OPTS);
32
57
  const tcStatus = tcResult.status;
33
58
  if (tcStatus === 'fail') {
34
59
  errors = errors.concat(extractErrorLinesJson(tcResult.output, /error TS\d+/));
@@ -41,7 +66,7 @@ if (testCmd === 'skip') {
41
66
  const testFiles = await resolveTestFiles(files, sessionStartRef);
42
67
  if (testFiles.length > 0) {
43
68
  const fileArgs = testFiles.join(' ');
44
- const testResult = await runCheck(`${testCmd} -- ${fileArgs}`);
69
+ const testResult = await runCheck(`${testCmd} -- ${fileArgs}`, CHECK_OPTS);
45
70
  testStatus = testResult.status;
46
71
  if (testStatus === 'fail') {
47
72
  const testErrors = extractErrorLinesJson(testResult.output, /(fail|error|FAIL)/i);
@@ -49,7 +74,7 @@ if (testCmd === 'skip') {
49
74
  }
50
75
  } else if (!files && !sessionStartRef) {
51
76
  // No FILES or SESSION_START_REF supplied: run the full test suite
52
- const testResult = await runCheck(testCmd);
77
+ const testResult = await runCheck(testCmd, CHECK_OPTS);
53
78
  testStatus = testResult.status;
54
79
  if (testStatus === 'fail') {
55
80
  const testErrors = extractErrorLinesJson(testResult.output, /(fail|error|FAIL)/i);
@@ -14,6 +14,31 @@ import { runCheck, csvToJsonArray } from './gate-helpers.mjs';
14
14
  const typecheckCmd = process.env.TYPECHECK_CMD;
15
15
  const testCmd = process.env.TEST_CMD;
16
16
 
17
+ /**
18
+ * Per-command wall-clock ceiling, published by `scripts/run-quality-gate.mjs`
19
+ * as `GATE_TIMEOUT_MS` (#1425 A3 / #1432). It carries the ALREADY-RESOLVED
20
+ * value — operator override `SO_GATE_TIMEOUT_MS` > Session Config
21
+ * `gate.timeout-path-b-ms` > 900 000 — so this script only has to read it.
22
+ *
23
+ * Absent or non-numeric (this gate invoked directly, not through the wrapper)
24
+ * → the option is OMITTED, and `runCheck` falls back to `resolveGateTimeoutMs()`
25
+ * exactly as before. An empty object is deliberate: passing `timeoutMs:
26
+ * undefined` would NOT trigger that fallback in every spread order.
27
+ */
28
+ const CHECK_OPTS = (() => {
29
+ const raw = Number((process.env.GATE_TIMEOUT_MS || '').trim());
30
+ const opts = Number.isFinite(raw) && raw > 0 ? { timeoutMs: raw } : {};
31
+ // GATE_LEDGER_ROOT (#1425 A4, W5 fix-pass): the wrapper publishes the ledger
32
+ // root it resolved (--ledger-root > repo root) so the gate-process register
33
+ // is written where the WRAPPER decided, not where this sub-script happens to
34
+ // run. Without it every runCheck() here defaulted to process.cwd(), and a test
35
+ // that spawned this script from the checkout wrote real lines into the live
36
+ // .orchestrator/runtime/gate-processes.jsonl — the reaper's kill population.
37
+ const ledgerRoot = (process.env.GATE_LEDGER_ROOT || "").trim();
38
+ if (ledgerRoot && ledgerRoot.startsWith("/")) opts.repoRoot = ledgerRoot;
39
+ return opts;
40
+ })();
41
+
17
42
  if (!typecheckCmd) {
18
43
  process.stderr.write('gate-per-file: TYPECHECK_CMD must be set\n');
19
44
  process.exit(1);
@@ -33,14 +58,14 @@ if (files.length === 0) {
33
58
  }
34
59
 
35
60
  // Typecheck runs on the whole project, not per-file
36
- const tcResult = runCheck(typecheckCmd);
61
+ const tcResult = await runCheck(typecheckCmd, CHECK_OPTS);
37
62
  const tcStatus = tcResult.status;
38
63
 
39
64
  let testStatus = 'skip';
40
65
 
41
66
  if (testCmd !== 'skip' && files.length > 0) {
42
67
  const fileArgs = files.join(' ');
43
- const testResult = runCheck(`${testCmd} -- ${fileArgs}`);
68
+ const testResult = await runCheck(`${testCmd} -- ${fileArgs}`, CHECK_OPTS);
44
69
  testStatus = testResult.status;
45
70
  }
46
71
 
@@ -24,6 +24,7 @@ import { promisify } from 'node:util';
24
24
 
25
25
  import { detectVcsForRepo, discoverVaultRepos } from '../gitlab-portfolio/vcs-detect.mjs';
26
26
  import { resolveRepoSpec } from '../vcs-repo-spec.mjs';
27
+ import { isMainModule } from '../is-main-module.mjs';
27
28
 
28
29
  const realExecFile = promisify(execFileCb);
29
30
 
@@ -455,7 +456,7 @@ export async function main(argv, deps = {}) {
455
456
 
456
457
  // ── CLI guard — prevent process.exit during test-time imports ─────────────────
457
458
 
458
- if (import.meta.url === `file://${process.argv[1]}`) {
459
+ if (isMainModule(import.meta.url)) {
459
460
  main(process.argv.slice(2))
460
461
  .then((result) => {
461
462
  process.exit(result.exitCode);
@@ -36,6 +36,7 @@ import { expandTilde } from '../common.mjs';
36
36
  import { discoverVaultRepos } from './vcs-detect.mjs';
37
37
  import { fetchIssuesMultiRepo, summarizeRepo } from './aggregator.mjs';
38
38
  import { renderPortfolio, writePortfolio } from './markdown-writer.mjs';
39
+ import { isMainModule } from '../is-main-module.mjs';
39
40
 
40
41
  // ── Helpers ───────────────────────────────────────────────────────────────────
41
42
 
@@ -406,7 +407,7 @@ EXIT CODES
406
407
 
407
408
  // ── CLI guard — prevent process.exit during test-time imports ─────────────────
408
409
 
409
- if (import.meta.url === `file://${process.argv[1]}`) {
410
+ if (isMainModule(import.meta.url)) {
410
411
  main(process.argv.slice(2)).then((result) => {
411
412
  process.exit(result.exitCode);
412
413
  }).catch((err) => {
@@ -139,7 +139,12 @@ export function renderPortfolio(summaries, opts) {
139
139
  // Frontmatter
140
140
  lines.push('---');
141
141
  lines.push(`_generator: ${GENERATOR_MARKER}`);
142
- lines.push(`type: dashboard`);
142
+ // `id` + `type` satisfy the vault frontmatter schema (skills/vault-sync/validator.mjs).
143
+ // `type: dashboard` was not in its enum and `id` was missing, so the generated file
144
+ // failed the hard gate at /close (#1144). `board` is the enum member added for
145
+ // generated dashboards (#738) — same shape as vault-status/board-writer.mjs.
146
+ lines.push('id: portfolio');
147
+ lines.push('type: board');
143
148
  lines.push(`created: ${createdValue}`);
144
149
  lines.push(`updated: ${updatedValue}`);
145
150
  lines.push('---');