session-orchestrator 5.2.0 → 5.4.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 (427) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
  9. package/.agents/skills/close/SKILL.md +5 -1
  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 +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -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 +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  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 +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  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 +5 -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/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -8,10 +8,265 @@
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
+ * The INNER kill ladder of one gate command, in the gate envelope's own
128
+ * snake_case shape (#1457 point 1).
129
+ *
130
+ * `runCheck` knows, per command, whether its per-command ceiling fired, which
131
+ * signals it sent and which pids survived the SIGKILL step. Until this helper
132
+ * that knowledge died inside the gate sub-script, so a command killed by the
133
+ * INNER ceiling reached `orchestrator.quality_gate.failed` as
134
+ * `timed_out: false`. Spread it into the command's envelope object.
135
+ *
136
+ * Absent is not zero: a result that spawned nothing (skip, stub) carries no
137
+ * `timedOut` boolean, and gets NO keys — never `false` / `[]`.
138
+ *
139
+ * @param {{timedOut?: boolean, killSignals?: string[], survivors?: number[]}} result
140
+ * A {@link runCheck} result.
141
+ * @returns {{timed_out: boolean, kill_signals: string[], survivors: number[]}|{}}
142
+ * `survivors` are the pids, same as {@link gateTimeoutEnvelope}.
143
+ */
144
+ export function commandKillFields(result) {
145
+ if (typeof result?.timedOut !== 'boolean') return {};
146
+ return {
147
+ timed_out: result.timedOut,
148
+ kill_signals: Array.isArray(result.killSignals) ? [...result.killSignals] : [],
149
+ survivors: Array.isArray(result.survivors) ? [...result.survivors] : [],
150
+ };
151
+ }
152
+
153
+ /** Envelope keys, in execution order, that may carry {@link commandKillFields}. */
154
+ const KILL_FIELD_COMMANDS = ['typecheck', 'test', 'lint'];
155
+
156
+ /**
157
+ * Join the OUTER kill ladder (the gate sub-script as a whole) with the INNER
158
+ * per-command ladders its JSON envelope publishes (#1457 point 1), into the
159
+ * `timed_out` / `survivors` / `kill_signals` keys of
160
+ * `orchestrator.quality_gate.{passed,failed}`.
161
+ *
162
+ * - inner measured (the envelope parses and EVERY command object either carries
163
+ * a boolean `timed_out` or provably spawned nothing — `status: 'skip'` or
164
+ * listed in `stubbed`): `timed_out` = outer OR any inner; `kill_signals` =
165
+ * outer followed by each command's, in execution order; `survivors` = outer
166
+ * count plus every inner pid count.
167
+ * - inner NOT measured (a variant whose envelope does not publish it, an
168
+ * unparseable envelope, or the outer ceiling killed the sub-script before it
169
+ * wrote one): the outer ladder alone when it fired — a kill is a kill — and
170
+ * otherwise NO keys, because `false` / `0` / `[]` would claim that no
171
+ * command was killed when nobody looked.
172
+ *
173
+ * Never throws.
174
+ *
175
+ * @param {object} args
176
+ * @param {{timed_out: boolean, survivors: number, kill_signals: string[]}} args.outer
177
+ * `gateKillFields()` of the sub-script's `spawnInGroup` result.
178
+ * @param {string} args.gateStdout The sub-script's stdout envelope (`''` on an outer kill).
179
+ * @returns {{timed_out: boolean, survivors: number, kill_signals: string[]}|{}}
180
+ */
181
+ export function joinGateKillFields({ outer, gateStdout }) {
182
+ let parsed = null;
183
+ if (typeof gateStdout === 'string' && gateStdout.trim()) {
184
+ try {
185
+ parsed = JSON.parse(gateStdout);
186
+ } catch {
187
+ parsed = null;
188
+ }
189
+ }
190
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
191
+ const names = isObj(parsed) ? KILL_FIELD_COMMANDS.filter((name) => isObj(parsed[name])) : [];
192
+ const stubbed = isObj(parsed?.stubbed) ? parsed.stubbed : {};
193
+ // A command either published its ladder, or provably spawned nothing (skip /
194
+ // stub) — nothing killed is then a measurement too. Any OTHER command (a
195
+ // variant that ran it but does not publish the ladder) makes the inner side
196
+ // unknown as a whole.
197
+ const inner = names.map((name) => parsed[name]).filter((c) => typeof c.timed_out === 'boolean');
198
+ const innerKnown = names.length > 0 && names.every((name) => typeof parsed[name].timed_out === 'boolean'
199
+ || parsed[name].status === 'skip'
200
+ || Boolean(stubbed[name]));
201
+
202
+ if (!innerKnown) return outer?.timed_out === true ? { ...outer } : {};
203
+
204
+ return {
205
+ timed_out: outer?.timed_out === true || inner.some((c) => c.timed_out),
206
+ survivors: (outer?.survivors ?? 0)
207
+ + inner.reduce((n, c) => n + (Array.isArray(c.survivors) ? c.survivors.length : 0), 0),
208
+ kill_signals: [
209
+ ...(outer?.kill_signals ?? []),
210
+ ...inner.flatMap((c) => (Array.isArray(c.kill_signals) ? c.kill_signals : [])),
211
+ ],
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Decide WHAT a finished gate sub-script run publishes — stdout, stderr,
217
+ * exit code and operator warnings — without performing any of the writes.
218
+ *
219
+ * ## Why this is a pure function and not four `process.*.write` calls
220
+ *
221
+ * The timeout branch in `scripts/run-quality-gate.mjs` was unreachable by any
222
+ * test: reaching it required a REAL gate sub-script to exceed
223
+ * `resolveGateTimeoutMs() + GATE_OUTER_TIMEOUT_RESERVE_MS` (15 min + 60 s, a
224
+ * hard constant with no injection seam), so the four decisions it makes —
225
+ * suppress the partial capture on stdout, re-publish it on stderr, emit ONE
226
+ * complete `gate-timeout` envelope, warn about survivors — were pinned by
227
+ * nothing. The CLI now decides here and only WRITES there, so each decision is
228
+ * testable against a synthetic {@link spawnInGroup} result.
229
+ *
230
+ * ## The one-document contract on stdout
231
+ *
232
+ * A killed child never wrote its envelope, so its capture is at best a partial
233
+ * JSON document. Publishing that hands every stdout consumer a parse error
234
+ * where a named failure belongs; publishing BOTH the partial text and an
235
+ * envelope breaks the "one JSON document" contract. Hence: capture → stderr,
236
+ * envelope → stdout, and `stdout` carries EXACTLY the envelope line.
237
+ *
238
+ * @param {object} args
239
+ * @param {{fullOutput?: string, exitCode?: number, timedOut?: boolean, pgid?: number,
240
+ * durationMs?: number, killSignals?: string[], survivors?: number[]}} args.result
241
+ * The {@link spawnInGroup} result for the gate sub-script.
242
+ * @param {string} args.variant The `--variant` value the run was started with.
243
+ * @param {number} args.timeoutMs The OUTER ceiling that applied to the sub-script.
244
+ * @returns {{stdout: string, stderr: string, exitCode: number, warnings: string[]}}
245
+ * `stdout`/`stderr` are written verbatim (empty string = write nothing);
246
+ * `warnings` go through the caller's `warn()`; `exitCode` is the gate's own,
247
+ * which `spawnInGroup` reports as 124 on the timeout path.
248
+ */
249
+ export function publishGateOutcome({ result, variant, timeoutMs }) {
250
+ const capture = String(result?.fullOutput ?? '');
251
+ const exitCode = result?.exitCode ?? 0;
252
+
253
+ if (!result?.timedOut) {
254
+ return { stdout: capture, stderr: '', exitCode, warnings: [] };
255
+ }
256
+
257
+ const survivors = result.survivors ?? [];
258
+ return {
259
+ stdout: `${JSON.stringify(gateTimeoutEnvelope({ variant, timeoutMs, run: result }))}\n`,
260
+ stderr: capture.trim()
261
+ ? `\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`
262
+ : '',
263
+ exitCode,
264
+ warnings: survivors.length > 0
265
+ ? [`gate process group ${result.pgid} left survivors after SIGKILL: ${survivors.join(', ')}`]
266
+ : [],
267
+ };
268
+ }
269
+
15
270
  // ---------------------------------------------------------------------------
16
271
  // Internal pattern helpers
17
272
  // ---------------------------------------------------------------------------
@@ -39,7 +294,8 @@ function isTestFile(filePath) {
39
294
  // ---------------------------------------------------------------------------
40
295
 
41
296
  /**
42
- * Execute a shell command and return a structured result.
297
+ * Execute a shell command as the LEADER OF ITS OWN PROCESS GROUP, under a
298
+ * wall-clock ceiling, and return a structured result.
43
299
  *
44
300
  * `output` is a bounded TAIL for humans. `fullOutput` is the complete captured
45
301
  * text and is what every COUNT parse must read.
@@ -52,10 +308,47 @@ function isTestFile(filePath) {
52
308
  * "the runner never produced results" and hides WHICH test failed. An hour was
53
309
  * spent chasing that phantom before the real cause (one red test) was found.
54
310
  *
311
+ * ## Why a process group, and why a timeout (Epic #1425 A3)
312
+ *
313
+ * This used to be `execSync(cmd, { maxBuffer })` — no timeout at all, and the
314
+ * shell as the only signalled process. On 2026-09-20 four `tsgo --noEmit`
315
+ * grandchildren of gate runs outlived their parents at PPID 1 with up to 8.0 GB
316
+ * RSS each and took the host to 13 % free memory. {@link spawnInGroup} spawns
317
+ * `detached`, so `process.kill(-pgid, …)` reaches every descendant, and it runs
318
+ * the SIGTERM→grace→SIGKILL ladder with a read-back verification.
319
+ *
320
+ * ## What changed for callers
321
+ *
322
+ * 1. It is ASYNC. Every call site must `await`.
323
+ * 2. `fullOutput` now interleaves stdout AND stderr on the PASS path too
324
+ * (`execSync` discarded stderr when the command succeeded). A runner that
325
+ * prints its summary to stderr is therefore no longer invisible to
326
+ * {@link extractTestCounts}.
327
+ * 3. Three fields are added — `timedOut`, `killSignals`, `survivors` — and they
328
+ * are present ONLY when a process actually ran. A skipped or stubbed command
329
+ * spawned nothing, so it carries no `timedOut: false`: absent is not a
330
+ * measured false, the same contract `counts` and `files` already keep.
331
+ *
332
+ * A timeout is a REPORTED failure: `status: 'fail'`, `exitCode: 124`, and a
333
+ * `gate: TIMEOUT after …` line appended to `output`/`fullOutput` naming the
334
+ * ceiling, the group, the signal ladder and any survivor. It is never a silent
335
+ * `fail`.
336
+ *
55
337
  * @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' } }}
338
+ * @param {object} [opts] - Forwarded verbatim to {@link spawnInGroup}, and it
339
+ * OVERRIDES the defaults below (`timeoutMs`, `maxOutputBytes`, `repoRoot`,
340
+ * `commandSignature`). This is also the seam tests inject `spawnFn` /
341
+ * `killFn` / `isAliveFn` through.
342
+ * @param {number|null} [opts.timeoutMs] - Wall-clock ceiling; defaults to
343
+ * {@link resolveGateTimeoutMs} (`SO_GATE_TIMEOUT_MS` or 900_000). `null`
344
+ * disables the clock — the byte cap still applies.
345
+ * @param {string} [opts.repoRoot] - Root whose gate-process ledger the spawn is
346
+ * recorded in; defaults to `process.cwd()`, which is the tree under test.
347
+ * @returns {Promise<{ status: 'pass'|'fail'|'skip', output: string, fullOutput: string,
348
+ * exitCode: number, timedOut?: boolean, killSignals?: string[], survivors?: number[],
349
+ * stubbed?: { kind: 'echo'|'noop' } }>}
57
350
  */
58
- export function runCheck(cmd) {
351
+ export async function runCheck(cmd, opts = {}) {
59
352
  if (!cmd || cmd === 'skip') {
60
353
  return { status: 'skip', output: '', fullOutput: '', exitCode: 0 };
61
354
  }
@@ -65,26 +358,44 @@ export function runCheck(cmd) {
65
358
  return { status: 'pass', output: `(stubbed: ${stub.kind})`, fullOutput: `(stubbed: ${stub.kind})`, exitCode: 0, stubbed: { kind: stub.kind } };
66
359
  }
67
360
 
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
- }
361
+ const timeoutMs = opts.timeoutMs === undefined ? resolveGateTimeoutMs() : opts.timeoutMs;
362
+ const run = await spawnInGroup(cmd, {
363
+ maxOutputBytes: RUN_CHECK_MAX_BUFFER_BYTES,
364
+ repoRoot: process.cwd(),
365
+ commandSignature: buildCommandSignature(cmd),
366
+ ...opts,
367
+ timeoutMs,
368
+ });
369
+
370
+ // Reported in every returned shape below, so a consumer never has to ask a
371
+ // second question to learn whether the group is actually gone.
372
+ const groupFields = { killSignals: run.killSignals, survivors: run.survivors };
373
+
374
+ if (run.timedOut) {
375
+ const fullOutput = `${run.fullOutput}\n${timeoutLine(timeoutMs, run)}\n`;
376
+ return {
377
+ status: 'fail',
378
+ output: tailOf(fullOutput),
379
+ fullOutput,
380
+ exitCode: run.exitCode,
381
+ timedOut: true,
382
+ ...groupFields,
383
+ };
384
+ }
83
385
 
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 };
386
+ // Exit code 127 means command not found — treat as skip.
387
+ if (run.exitCode === 127) {
388
+ return { status: 'skip', output: 'command not found', fullOutput: '', exitCode: 127, timedOut: false, ...groupFields };
87
389
  }
390
+
391
+ return {
392
+ status: run.exitCode === 0 ? 'pass' : 'fail',
393
+ output: tailOf(run.fullOutput),
394
+ fullOutput: run.fullOutput,
395
+ exitCode: run.exitCode,
396
+ timedOut: false,
397
+ ...groupFields,
398
+ };
88
399
  }
89
400
 
90
401
  /**
@@ -392,8 +703,10 @@ export function extractFailedTestFiles(output, cwd = process.cwd()) {
392
703
  *
393
704
  * This function sees a candidate triple and nothing else. It performs no I/O and
394
705
  * reads no files. The following are per-caller INPUT ADAPTERS and must not
395
- * migrate here: JSON-envelope parsing; the `test`-object-vs-status-string
396
- * variant discrimination (non-full-gate variants emit a bare status string);
706
+ * migrate here: JSON-envelope parsing; the `test`-must-be-an-object check
707
+ * (every variant emits an object since #1457 pkt 12; only full-gate's carries
708
+ * counts, so the others fall through to this function's own finite-number
709
+ * refusal);
397
710
  * `test.status ∈ {pass, fail}`; the `parsed.stubbed?.test` short-circuit; the
398
711
  * raw-text tail parse ({@link extractTestCounts}); and the positional evidence
399
712
  * that the test gate ran at all — which each caller expresses by handing over
@@ -6,13 +6,38 @@
6
6
  // Required env: TYPECHECK_CMD, TEST_CMD
7
7
  // Optional env: FILES (comma-separated), SESSION_START_REF
8
8
 
9
- import { runCheck, resolveTestFiles, extractErrorLinesJson } from './gate-helpers.mjs';
9
+ import { runCheck, resolveTestFiles, extractErrorLinesJson, commandKillFields } from './gate-helpers.mjs';
10
10
 
11
11
  const typecheckCmd = process.env.TYPECHECK_CMD;
12
12
  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);
@@ -24,41 +49,32 @@ if (!testCmd) {
24
49
 
25
50
  const start = Date.now();
26
51
 
27
- let testStatus;
52
+ // A skipped test run spawned nothing: `{ status: 'skip' }`, no kill keys.
53
+ let testResult = { status: 'skip' };
28
54
  let errors = [];
29
55
 
30
56
  // --- typecheck (always runs unless cmd is "skip") ---
31
- const tcResult = await runCheck(typecheckCmd);
32
- const tcStatus = tcResult.status;
33
- if (tcStatus === 'fail') {
57
+ const tcResult = await runCheck(typecheckCmd, CHECK_OPTS);
58
+ if (tcResult.status === 'fail') {
34
59
  errors = errors.concat(extractErrorLinesJson(tcResult.output, /error TS\d+/));
35
60
  }
36
61
 
37
62
  // --- test (scoped to changed/specified files) ---
38
- if (testCmd === 'skip') {
39
- testStatus = 'skip';
40
- } else {
63
+ if (testCmd !== 'skip') {
41
64
  const testFiles = await resolveTestFiles(files, sessionStartRef);
42
65
  if (testFiles.length > 0) {
43
66
  const fileArgs = testFiles.join(' ');
44
- const testResult = await runCheck(`${testCmd} -- ${fileArgs}`);
45
- testStatus = testResult.status;
46
- if (testStatus === 'fail') {
47
- const testErrors = extractErrorLinesJson(testResult.output, /(fail|error|FAIL)/i);
48
- errors = errors.concat(testErrors);
49
- }
67
+ testResult = await runCheck(`${testCmd} -- ${fileArgs}`, CHECK_OPTS);
50
68
  } else if (!files && !sessionStartRef) {
51
69
  // No FILES or SESSION_START_REF supplied: run the full test suite
52
- const testResult = await runCheck(testCmd);
53
- testStatus = testResult.status;
54
- if (testStatus === 'fail') {
55
- const testErrors = extractErrorLinesJson(testResult.output, /(fail|error|FAIL)/i);
56
- errors = errors.concat(testErrors);
57
- }
70
+ testResult = await runCheck(testCmd, CHECK_OPTS);
58
71
  } else {
59
72
  // Files/ref supplied but no test files found — skip
60
73
  process.stderr.write('warn: No test files found for incremental run; skipping tests\n');
61
- testStatus = 'skip';
74
+ }
75
+ if (testResult.status === 'fail') {
76
+ const testErrors = extractErrorLinesJson(testResult.output, /(fail|error|FAIL)/i);
77
+ errors = errors.concat(testErrors);
62
78
  }
63
79
  }
64
80
 
@@ -67,8 +83,18 @@ const duration_seconds = Math.round((Date.now() - start) / 1000);
67
83
  const result = {
68
84
  variant: 'incremental',
69
85
  duration_seconds,
70
- typecheck: tcStatus,
71
- test: testStatus,
86
+ // Each command object also carries its INNER kill ladder (`timed_out`,
87
+ // `kill_signals`, `survivors` pids) when `runCheck` spawned it — omitted for
88
+ // skip/stub (#1457). `run-quality-gate.mjs` joins them into its event.
89
+ typecheck: { status: tcResult.status, ...commandKillFields(tcResult) },
90
+ test: { status: testResult.status, ...commandKillFields(testResult) },
91
+ // Stub map, same shape as gate-full: a command `runCheck` recognised as a
92
+ // stub (`echo …`/`true`) spawned nothing, so `joinGateKillFields` may count
93
+ // it as measured instead of declaring the whole inner ladder unknown.
94
+ stubbed: {
95
+ ...(tcResult.stubbed ? { typecheck: tcResult.stubbed } : {}),
96
+ ...(testResult.stubbed ? { test: testResult.stubbed } : {}),
97
+ },
72
98
  errors,
73
99
  };
74
100
 
@@ -9,11 +9,36 @@
9
9
  * Part of v3.2 shell-helper port migration (issue #218 / #317).
10
10
  */
11
11
 
12
- import { runCheck, csvToJsonArray } from './gate-helpers.mjs';
12
+ import { runCheck, csvToJsonArray, commandKillFields } from './gate-helpers.mjs';
13
13
 
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,21 +58,30 @@ if (files.length === 0) {
33
58
  }
34
59
 
35
60
  // Typecheck runs on the whole project, not per-file
36
- const tcResult = runCheck(typecheckCmd);
37
- const tcStatus = tcResult.status;
61
+ const tcResult = await runCheck(typecheckCmd, CHECK_OPTS);
38
62
 
39
- let testStatus = 'skip';
63
+ // A skipped test run spawned nothing: `{ status: 'skip' }`, no kill keys.
64
+ let testResult = { status: 'skip' };
40
65
 
41
66
  if (testCmd !== 'skip' && files.length > 0) {
42
67
  const fileArgs = files.join(' ');
43
- const testResult = runCheck(`${testCmd} -- ${fileArgs}`);
44
- testStatus = testResult.status;
68
+ testResult = await runCheck(`${testCmd} -- ${fileArgs}`, CHECK_OPTS);
45
69
  }
46
70
 
47
71
  const result = {
48
72
  variant: 'per-file',
49
- typecheck: tcStatus,
50
- test: testStatus,
73
+ // Each command object also carries its INNER kill ladder (`timed_out`,
74
+ // `kill_signals`, `survivors` pids) when `runCheck` spawned it — omitted for
75
+ // skip/stub (#1457). `run-quality-gate.mjs` joins them into its event.
76
+ typecheck: { status: tcResult.status, ...commandKillFields(tcResult) },
77
+ test: { status: testResult.status, ...commandKillFields(testResult) },
78
+ // Stub map, same shape as gate-full: a command `runCheck` recognised as a
79
+ // stub (`echo …`/`true`) spawned nothing, so `joinGateKillFields` may count
80
+ // it as measured instead of declaring the whole inner ladder unknown.
81
+ stubbed: {
82
+ ...(tcResult.stubbed ? { typecheck: tcResult.stubbed } : {}),
83
+ ...(testResult.stubbed ? { test: testResult.stubbed } : {}),
84
+ },
51
85
  files,
52
86
  };
53
87
 
@@ -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('---');