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
@@ -23,9 +23,15 @@
23
23
  * 0 — pass (or informational; non-blocking variants always exit 0)
24
24
  * 1 — script error (bad arguments, missing dependencies)
25
25
  * 2 — gate failed (full-gate only: typecheck/test/lint errors)
26
+ * 124 — the gate sub-script exceeded the wall-clock ceiling and its whole
27
+ * process GROUP was killed (SIGTERM→SIGKILL). Same value coreutils
28
+ * `timeout(1)` uses. Applies to EVERY variant, including the otherwise
29
+ * non-blocking ones: a killed gate measured nothing, so reporting it as
30
+ * 0 would be a false green (Epic #1425 A3).
26
31
  *
27
32
  * The gate sub-scripts in scripts/lib/gates/ are NOT reimplemented here; they are
28
- * invoked via child_process.spawn('bash', [path, ...]) with the required env vars.
33
+ * invoked via spawnInGroup() — a detached, process-group-leading shell — with the
34
+ * required env vars.
29
35
  *
30
36
  * References:
31
37
  * scripts/run-quality-gate.sh — original shell orchestrator
@@ -37,13 +43,23 @@
37
43
  import { existsSync, readFileSync, statSync } from 'node:fs';
38
44
  import { join, dirname, resolve } from 'node:path';
39
45
  import { fileURLToPath } from 'node:url';
40
- import { spawnSync } from 'node:child_process';
46
+ import { spawn } from 'node:child_process';
41
47
 
42
48
  import { die, warn } from './lib/common.mjs';
43
49
  import { loadQualityGatesPolicy, resolveCommand } from './lib/quality-gates-policy.mjs';
44
50
  import { emitEvent, sessionAttribution } from './lib/events.mjs';
45
- import { admitSuiteCounts } from './lib/gates/gate-helpers.mjs';
51
+ import {
52
+ admitSuiteCounts,
53
+ joinGateKillFields,
54
+ publishGateOutcome,
55
+ resolveGateTimeoutMs,
56
+ } from './lib/gates/gate-helpers.mjs';
57
+ import { buildCommandSignature, spawnInGroup } from './lib/process-group.mjs';
58
+ import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
46
59
  import { findScopeFile } from './lib/scope-gate.mjs';
60
+ import { gateKillFields } from './lib/quality-gate.mjs';
61
+ import { isMainModule } from './lib/is-main-module.mjs';
62
+ import { shellQuote } from './lib/sh-quote.mjs';
47
63
 
48
64
  // ---------------------------------------------------------------------------
49
65
  // Constants
@@ -62,6 +78,24 @@ const VALID_VARIANTS = ['baseline', 'incremental', 'full-gate', 'per-file'];
62
78
  */
63
79
  const GATE_STDOUT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
64
80
 
81
+ /**
82
+ * Head-room between the ceiling the gate's OWN commands run under
83
+ * (`resolveGateTimeoutMs()`, applied per command inside `runCheck`) and the one
84
+ * this wrapper applies to the gate sub-script as a whole.
85
+ *
86
+ * The inner cap must fire FIRST: it kills exactly the wedged command and still
87
+ * lets the gate print its JSON envelope, name the failing check on stderr and
88
+ * exit 2. This outer cap is the backstop for the case the inner one cannot
89
+ * reach — a gate wedged outside `runCheck`, or a kill ladder that left a
90
+ * survivor holding the pipe open.
91
+ *
92
+ * Named ceiling (BV-004): 60 s. It only has to cover one full SIGTERM→grace→
93
+ * SIGKILL→verify ladder (`DEFAULT_KILL_GRACE_MS` 10 s + 0.5 s verify + 1 s
94
+ * deadline slack ≈ 11.5 s) plus the gate's own JSON write. Revisit if the grace
95
+ * period is ever raised past ~45 s.
96
+ */
97
+ const GATE_OUTER_TIMEOUT_RESERVE_MS = 60_000;
98
+
65
99
  const DEFAULT_TEST_CMD = 'npm test';
66
100
  const DEFAULT_TYPECHECK_CMD = 'npm run typecheck';
67
101
  const DEFAULT_LINT_CMD = 'npm run lint';
@@ -74,63 +108,6 @@ const GATE_SCRIPT = {
74
108
  'per-file': join(GATES_DIR, 'gate-per-file.mjs'),
75
109
  };
76
110
 
77
- // ---------------------------------------------------------------------------
78
- // Argument parsing
79
- // ---------------------------------------------------------------------------
80
-
81
- const argv = process.argv.slice(2);
82
-
83
- if (argv.includes('-h') || argv.includes('--help')) {
84
- process.stdout.write(
85
- 'Usage: run-quality-gate.mjs --variant <variant> [--config <json-or-file>] ' +
86
- '[--files <file1,file2,...>] [--session-start-ref <ref>] [--ledger-root <path>]\n\n' +
87
- 'Variants: baseline, incremental, full-gate, per-file\n\n' +
88
- 'Exit codes:\n' +
89
- ' 0 — pass (non-blocking variants always exit 0)\n' +
90
- ' 1 — script error (bad arguments, missing dependencies)\n' +
91
- ' 2 — gate failed (full-gate only)\n',
92
- );
93
- process.exit(0);
94
- }
95
-
96
- let variant = '';
97
- let config = '';
98
- let files = '';
99
- let sessionStartRef = '';
100
- let ledgerRootArg = '';
101
-
102
- for (let i = 0; i < argv.length; i++) {
103
- const arg = argv[i];
104
- switch (arg) {
105
- case '--variant':
106
- if (i + 1 >= argv.length) die('Missing value for --variant');
107
- variant = argv[++i];
108
- break;
109
- case '--config':
110
- if (i + 1 >= argv.length) die('Missing value for --config');
111
- config = argv[++i];
112
- break;
113
- case '--files':
114
- if (i + 1 >= argv.length) die('Missing value for --files');
115
- files = argv[++i];
116
- break;
117
- case '--session-start-ref':
118
- if (i + 1 >= argv.length) die('Missing value for --session-start-ref');
119
- sessionStartRef = argv[++i];
120
- break;
121
- case '--ledger-root':
122
- if (i + 1 >= argv.length) die('Missing value for --ledger-root');
123
- ledgerRootArg = argv[++i];
124
- break;
125
- default:
126
- die(`Unknown argument: ${arg}`);
127
- }
128
- }
129
-
130
- if (!variant) die('Missing required argument: --variant');
131
- if (!VALID_VARIANTS.includes(variant)) {
132
- die(`Invalid variant: '${variant}' (allowed: ${VALID_VARIANTS.join(', ')})`);
133
- }
134
111
 
135
112
  // ---------------------------------------------------------------------------
136
113
  // Command resolution — policy-file-first (#183), then config, then defaults
@@ -179,9 +156,11 @@ function extractCommand(policy, policyKey, configKey, configJson, defaultCmd) {
179
156
  * The four rejections that stay here are envelope-shaped, not numeric:
180
157
  *
181
158
  * 1. stdout is absent or not parseable JSON;
182
- * 2. `test` is not the object form — only `gate-full.mjs` reports numbers;
183
- * `gate-{baseline,incremental,per-file}.mjs` emit a bare status STRING, so
184
- * a non-full-gate variant structurally cannot carry counts;
159
+ * 2. `test` is not an object. Every variant emits `test` as an object since
160
+ * #1457 pkt 12, but only `gate-full.mjs` puts counts in it —
161
+ * `gate-{baseline,incremental,per-file}.mjs` publish `{ status, …kill
162
+ * ladder }` only, which {@link admitSuiteCounts} refuses for lack of a
163
+ * finite `passed` / `total`;
185
164
  * 3. the test gate was skipped (`status` neither `pass` nor `fail`);
186
165
  * 4. the test COMMAND was detected as a stub (`echo …` / no-op) — a stub's
187
166
  * output parses to 0/0, which would be a fabricated zero.
@@ -225,7 +204,8 @@ function suiteCountsFromGateStdout(stdout) {
225
204
  * the key instead of publishing an empty array that reads as "no file failed".
226
205
  *
227
206
  * Only `gate-full.mjs` publishes the key, and only when the runner printed a
228
- * file-level summary; every other variant emits a bare status string.
207
+ * file-level summary; every other variant's `test` object carries only
208
+ * `status` and the kill ladder.
229
209
  *
230
210
  * Never throws.
231
211
  *
@@ -319,147 +299,327 @@ function resolveWaveNumber(projectDir) {
319
299
  }
320
300
  }
321
301
 
322
- // Load policy file (never throws)
323
- const repoRoot = process.cwd();
324
- const policy = loadQualityGatesPolicy(repoRoot);
325
-
326
- // Parse --config (JSON string or file path)
327
- let configJson = null;
328
- if (config) {
329
- if (existsSync(config)) {
330
- try {
331
- configJson = JSON.parse(readFileSync(config, 'utf8'));
332
- } catch (err) {
333
- warn(`Could not parse config file '${config}': ${err.message}; using defaults`);
302
+ // ---------------------------------------------------------------------------
303
+ // Main — the CLI body. Guarded below so a bare `import()` runs none of it.
304
+ // ---------------------------------------------------------------------------
305
+
306
+ async function main() {
307
+ // Argument parsing
308
+ const argv = process.argv.slice(2);
309
+
310
+ if (argv.includes('-h') || argv.includes('--help')) {
311
+ process.stdout.write(
312
+ 'Usage: run-quality-gate.mjs --variant <variant> [--config <json-or-file>] ' +
313
+ '[--files <file1,file2,...>] [--session-start-ref <ref>] [--ledger-root <path>]\n\n' +
314
+ 'Variants: baseline, incremental, full-gate, per-file\n\n' +
315
+ 'Exit codes:\n' +
316
+ ' 0 — pass (non-blocking variants always exit 0)\n' +
317
+ ' 1 — script error (bad arguments, missing dependencies)\n' +
318
+ ' 2 — gate failed (full-gate only)\n' +
319
+ ' 124 — gate timed out; its process group was killed (SIGTERM→SIGKILL)\n',
320
+ );
321
+ process.exit(0);
322
+ }
323
+
324
+ let variant = '';
325
+ let config = '';
326
+ let files = '';
327
+ let sessionStartRef = '';
328
+ let ledgerRootArg = '';
329
+
330
+ for (let i = 0; i < argv.length; i++) {
331
+ const arg = argv[i];
332
+ switch (arg) {
333
+ case '--variant':
334
+ if (i + 1 >= argv.length) die('Missing value for --variant');
335
+ variant = argv[++i];
336
+ break;
337
+ case '--config':
338
+ if (i + 1 >= argv.length) die('Missing value for --config');
339
+ config = argv[++i];
340
+ break;
341
+ case '--files':
342
+ if (i + 1 >= argv.length) die('Missing value for --files');
343
+ files = argv[++i];
344
+ break;
345
+ case '--session-start-ref':
346
+ if (i + 1 >= argv.length) die('Missing value for --session-start-ref');
347
+ sessionStartRef = argv[++i];
348
+ break;
349
+ case '--ledger-root':
350
+ if (i + 1 >= argv.length) die('Missing value for --ledger-root');
351
+ ledgerRootArg = argv[++i];
352
+ break;
353
+ default:
354
+ die(`Unknown argument: ${arg}`);
334
355
  }
335
- } else {
336
- try {
337
- configJson = JSON.parse(config);
338
- } catch {
339
- warn('Config is neither a valid file path nor valid JSON; using defaults');
356
+ }
357
+
358
+ if (!variant) die('Missing required argument: --variant');
359
+ if (!VALID_VARIANTS.includes(variant)) {
360
+ die(`Invalid variant: '${variant}' (allowed: ${VALID_VARIANTS.join(', ')})`);
361
+ }
362
+
363
+ // Load policy file (never throws)
364
+ const repoRoot = process.cwd();
365
+ const policy = loadQualityGatesPolicy(repoRoot);
366
+
367
+ // Parse --config (JSON string or file path)
368
+ let configJson = null;
369
+ if (config) {
370
+ if (existsSync(config)) {
371
+ try {
372
+ configJson = JSON.parse(readFileSync(config, 'utf8'));
373
+ } catch (err) {
374
+ warn(`Could not parse config file '${config}': ${err.message}; using defaults`);
375
+ }
376
+ } else {
377
+ try {
378
+ configJson = JSON.parse(config);
379
+ } catch {
380
+ warn('Config is neither a valid file path nor valid JSON; using defaults');
381
+ }
340
382
  }
341
383
  }
342
- }
343
384
 
344
- const TYPECHECK_CMD = extractCommand(policy, 'typecheck', 'typecheck-command', configJson, DEFAULT_TYPECHECK_CMD);
345
- const TEST_CMD = extractCommand(policy, 'test', 'test-command', configJson, DEFAULT_TEST_CMD);
346
- const LINT_CMD = extractCommand(policy, 'lint', 'lint-command', configJson, DEFAULT_LINT_CMD);
385
+ const TYPECHECK_CMD = extractCommand(policy, 'typecheck', 'typecheck-command', configJson, DEFAULT_TYPECHECK_CMD);
386
+ const TEST_CMD = extractCommand(policy, 'test', 'test-command', configJson, DEFAULT_TEST_CMD);
387
+ const LINT_CMD = extractCommand(policy, 'lint', 'lint-command', configJson, DEFAULT_LINT_CMD);
347
388
 
348
- // ---------------------------------------------------------------------------
349
- // Gate dispatch — shell-out to existing gate-*.mjs sub-scripts
350
- // ---------------------------------------------------------------------------
389
+ // ---------------------------------------------------------------------------
390
+ // Gate dispatch — shell-out to existing gate-*.mjs sub-scripts
391
+ // ---------------------------------------------------------------------------
351
392
 
352
- const gatePath = GATE_SCRIPT[variant];
393
+ const gatePath = GATE_SCRIPT[variant];
353
394
 
354
- if (!existsSync(gatePath)) {
355
- die(`Gate script not found: ${gatePath}`);
356
- }
395
+ if (!existsSync(gatePath)) {
396
+ die(`Gate script not found: ${gatePath}`);
397
+ }
357
398
 
358
- // `npm_config_loglevel` is INHERITED by every descendant, and the pre-push hook
359
- // invokes this script as `npm run --silent quality-gate` — which sets it to
360
- // `silent`. That level then reached the gate's own children: `npm pack
361
- // --dry-run` emitted ZERO `npm notice` lines instead of 818 (measured
362
- // 2026-08-22), so the release leakage test saw an empty listing, and several
363
- // validate-plugin/e2e tests that shell out to npm went red the same way. Every
364
- // one of them passes under a bare `npm test` and fails only INSIDE the gate,
365
- // which is the hardest shape to diagnose and cost an hour of chasing phantoms.
366
- //
367
- // Pinned rather than deleted: an explicit level makes the gate's children
368
- // independent of how the gate itself was invoked. `--silent` still does its real
369
- // job — keeping THIS process's stdout to the single JSON envelope — because the
370
- // children's output is captured by `runCheck`, never streamed.
371
- const env = {
372
- ...process.env,
373
- npm_config_loglevel: 'notice',
374
- TYPECHECK_CMD,
375
- TEST_CMD,
376
- LINT_CMD,
377
- FILES: files,
378
- SESSION_START_REF: sessionStartRef,
379
- };
399
+ // Resolved BEFORE the spawn (it used to sit beside the telemetry block below):
400
+ // the gate-process ledger the spawn writes is pinned to the same root as the
401
+ // event, for the same reason — see the spawn's `repoRoot` comment.
402
+ const ledgerRoot = resolveLedgerRoot(ledgerRootArg);
403
+
404
+ // `npm_config_loglevel` is INHERITED by every descendant, and the pre-push hook
405
+ // invokes this script as `npm run --silent quality-gate` — which sets it to
406
+ // `silent`. That level then reached the gate's own children: `npm pack
407
+ // --dry-run` emitted ZERO `npm notice` lines instead of 818 (measured
408
+ // 2026-08-22), so the release leakage test saw an empty listing, and several
409
+ // validate-plugin/e2e tests that shell out to npm went red the same way. Every
410
+ // one of them passes under a bare `npm test` and fails only INSIDE the gate,
411
+ // which is the hardest shape to diagnose and cost an hour of chasing phantoms.
412
+ //
413
+ // Pinned rather than deleted: an explicit level makes the gate's children
414
+ // independent of how the gate itself was invoked. `--silent` still does its real
415
+ // job — keeping THIS process's stdout to the single JSON envelope — because the
416
+ // children's output is captured by `runCheck`, never streamed.
417
+ /**
418
+ * Per-command wall-clock ceiling for PATH B (#1425 A3 / #1432).
419
+ *
420
+ * Precedence, highest first:
421
+ * 1. `SO_GATE_TIMEOUT_MS` — the OPERATOR override. It is inherited by every
422
+ * descendant, so a gate child that re-derives the ceiling gets the same
423
+ * answer; `resolveGateTimeoutMs()` already reads it.
424
+ * 2. Session Config `gate.timeout-path-b-ms` — the COMMITTED default.
425
+ * 3. `DEFAULT_GATE_TIMEOUT_MS` (900 000), via `resolveGateTimeoutMs()`.
426
+ *
427
+ * Published to the gate sub-script as `GATE_TIMEOUT_MS` — a DIFFERENT name from
428
+ * the operator override on purpose: writing the resolved value back into
429
+ * `SO_GATE_TIMEOUT_MS` would make a committed default indistinguishable from an
430
+ * operator decision for every process further down the tree.
431
+ */
432
+ const configuredGateTimeoutMs = (() => {
433
+ const block = configJson !== null && typeof configJson === 'object' ? configJson.gate : null;
434
+ const raw = block && typeof block === 'object' ? block['timeout-path-b-ms'] : undefined;
435
+ return Number.isFinite(raw) && raw > 0 ? raw : null;
436
+ })();
437
+ const operatorTimeoutOverride = (process.env.SO_GATE_TIMEOUT_MS || '').trim() !== '';
438
+ const commandTimeoutMs = operatorTimeoutOverride || configuredGateTimeoutMs === null
439
+ ? resolveGateTimeoutMs()
440
+ : configuredGateTimeoutMs;
441
+
442
+ // Gate-process REGISTER root — the same precedence the telemetry destination
443
+ // uses below (`--ledger-root` > project-dir env > cwd), so a sandboxed test that
444
+ // only sets CLAUDE_PROJECT_DIR never writes register lines into the checkout.
445
+ const gateLedgerRoot =
446
+ ledgerRoot ?? process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot;
447
+
448
+ const env = {
449
+ ...process.env,
450
+ npm_config_loglevel: 'notice',
451
+ TYPECHECK_CMD,
452
+ TEST_CMD,
453
+ LINT_CMD,
454
+ GATE_TIMEOUT_MS: String(commandTimeoutMs),
455
+ // Same resolution the wrapper uses for its OWN spawn (`--ledger-root` > repo
456
+ // root): the gate sub-scripts pass it to runCheck() so every register line of
457
+ // one gate run lands in ONE ledger (#1425 A4, W5 fix-pass).
458
+ GATE_LEDGER_ROOT: gateLedgerRoot,
459
+ FILES: files,
460
+ SESSION_START_REF: sessionStartRef,
461
+ };
462
+
463
+ // The gate sub-script runs as the LEADER OF ITS OWN PROCESS GROUP under a
464
+ // wall-clock ceiling (Epic #1425 A3). Before this, `spawnSync('node', [gatePath])`
465
+ // had NO timeout at all and no group semantics: a wedged `tsgo` or vitest worker
466
+ // two levels down was reparented to PPID 1 and kept its memory (2026-09-20: four
467
+ // such orphans, up to 8.0 GB RSS each, host at 13 % free).
468
+ //
469
+ // stdout is PIPED (not inherited) so the suite counts the gate already computed
470
+ // can be lifted straight off its JSON envelope into telemetry (#954) instead of
471
+ // travelling as prose through the STATE.md header. The envelope is re-emitted
472
+ // verbatim below, so the stdout contract is unchanged — a gate sub-script writes
473
+ // exactly one JSON line at the very end (its own child commands are captured by
474
+ // `runCheck`), so nothing streamed before and nothing streams now.
475
+ //
476
+ // stderr stays INHERITED, and that is why `spawnFn` is overridden here rather
477
+ // than left at its default: `spawnInGroup` merges stdout and stderr into one
478
+ // capture, which would interleave the gate's failure disclosure (hundreds of
479
+ // lines, #1149) into the single JSON document every consumer parses off stdout.
480
+ // Handing it a child with `stderr: 'inherit'` leaves `child.stderr` null, the
481
+ // module's own `child.stderr?.on(…)` a no-op, and the gate's warnings live on
482
+ // the operator's terminal exactly as before.
483
+ // Quoted: the gate path derives from `import.meta.url`, so a space in the
484
+ // checkout path must not split the command.
485
+ const gateCommand = `node ${shellQuote(gatePath)}`;
486
+ // Derived from the SAME resolved per-command ceiling published as
487
+ // `GATE_TIMEOUT_MS` above — not from `resolveGateTimeoutMs()` a second time.
488
+ // The outer cap must stay strictly ABOVE the inner one so the inner kill fires
489
+ // first and the gate can still print its envelope; re-deriving here would leave
490
+ // the outer cap at the 900 000 default while a committed
491
+ // `gate.timeout-path-b-ms` above it made the inner cap the later of the two.
492
+ const gateTimeoutMs = commandTimeoutMs + GATE_OUTER_TIMEOUT_RESERVE_MS;
493
+ const result = await spawnInGroup(gateCommand, {
494
+ cwd: repoRoot,
495
+ env,
496
+ timeoutMs: gateTimeoutMs,
497
+ maxOutputBytes: GATE_STDOUT_MAX_BUFFER_BYTES,
498
+ // The ledger is what the orphan reaper (#1425 B) reads to tell its OWN gate
499
+ // processes from every other `node` on the host, so it is pinned to the same
500
+ // root the telemetry is — under the pre-push hook the tree the gate runs in
501
+ // is deleted seconds later, and the record with it.
502
+ repoRoot: gateLedgerRoot,
503
+ commandSignature: buildCommandSignature(gateCommand),
504
+ // The OWNER the ledger records, from the PROCESS-LOCAL witness only — the same
505
+ // rule `scripts/lib/quality-gate.mjs` follows for the gate commands it spawns
506
+ // (`.claude/rules/identity-and-locks.md`: rank witnesses, never union them).
507
+ // `.orchestrator/session.lock` is deliberately NOT consulted: it is a
508
+ // repo-GLOBAL artefact any session in this working copy may hold, so reading
509
+ // it would stamp a PEER's id onto our own gate process and point the orphan
510
+ // reaper (#1425 B) at the wrong owner. No witness → `null`, which the ledger
511
+ // records as "owner unknown" — the state every record carried before this.
512
+ sessionId: [...readProcessLocalSessionIds({ env: process.env, hookInput: null })][0] ?? null,
513
+ // stdin is IGNORED, not inherited. A gate sub-script reads no stdin, but a
514
+ // DETACHED child is in its own process group and is therefore not the
515
+ // terminal's foreground group: the first read from an inherited TTY earns it
516
+ // SIGTTIN, which stops the whole group until the outer ceiling kills it. An
517
+ // ignored stdin turns that hang into an immediate EOF.
518
+ spawnFn: (command, options) => spawn(command, { ...options, stdio: ['ignore', 'pipe', 'inherit'] }),
519
+ });
520
+
521
+ // `pid: -1` is `spawnInGroup`'s spawn-failure channel (it never rejects).
522
+ if (result.pid === -1) {
523
+ die(`Failed to run gate script: ${result.fullOutput.trim()}`);
524
+ }
380
525
 
381
- // stdout is PIPED (not inherited) so the suite counts the gate already computed
382
- // can be lifted straight off its JSON envelope into telemetry (#954) instead of
383
- // travelling as prose through the STATE.md header. The envelope is re-emitted
384
- // verbatim below, so the stdout contract is unchanged — a gate sub-script writes
385
- // exactly one JSON line at the very end (its own child commands are captured by
386
- // `runCheck`), so nothing streamed before and nothing streams now. stderr stays
387
- // inherited, keeping warnings live.
388
- const result = spawnSync('node', [gatePath], {
389
- env,
390
- stdio: ['inherit', 'pipe', 'inherit'],
391
- encoding: 'utf8',
392
- maxBuffer: GATE_STDOUT_MAX_BUFFER_BYTES,
393
- });
394
-
395
- const gateStdout = typeof result.stdout === 'string' ? result.stdout : '';
396
- if (gateStdout) process.stdout.write(gateStdout);
397
-
398
- if (result.error && typeof result.status !== 'number') {
399
- die(`Failed to run gate script: ${result.error.message}`);
526
+ // On timeout the child was KILLED before it could write its envelope, so its
527
+ // capture is at best a partial JSON document. Publishing that would hand every
528
+ // stdout consumer a parse error where a named failure belongs; publishing the
529
+ // partial text AND an envelope would break the one-document contract. So the
530
+ // capture goes to stderr, where the operator can still read it, and stdout
531
+ // carries a complete `gate-timeout` envelope instead.
532
+ //
533
+ // The DECISION lives in `publishGateOutcome` (gate-helpers.mjs) and only the
534
+ // WRITES live here: this branch needs a real 16-minute gate to reach, so while
535
+ // the decision was inline it was pinned by nothing.
536
+ const outcome = publishGateOutcome({ result, variant, timeoutMs: gateTimeoutMs });
537
+ const gateStdout = result.timedOut ? '' : result.fullOutput;
538
+ if (outcome.stderr) process.stderr.write(outcome.stderr);
539
+ if (outcome.stdout) process.stdout.write(outcome.stdout);
540
+ for (const line of outcome.warnings) warn(line);
541
+
542
+ // Quality-gate telemetry — one canonical event per gate run via emitEvent
543
+ // (single emission path). `sessionAttribution` is the shared helper in
544
+ // events.mjs (#941); this CLI wrapper runs against the CWD `repoRoot`, so the
545
+ // bare emitEvent destination (SO_PROJECT_DIR default) is correct here.
546
+ //
547
+ // EXCEPT under the pre-push hook, which is the one caller that runs the gate in
548
+ // a tree that is about to be DELETED. `.husky/pre-push` materialises the tracked
549
+ // tree into a temp dir and deliberately scrubs every `*PROJECT_DIR` name before
550
+ // invoking the gate there, so `getProjectDir()` resolves to that temp tree (it
551
+ // carries both a CLAUDE.md (or AGENTS.md) and a .git) and the record lands in
552
+ // `<tmp>/.orchestrator/metrics/events.jsonl`, which the hook's EXIT trap then
553
+ // removes. Measured 2026-09-06: a pre-push run that BLOCKED a push left no
554
+ // `orchestrator.quality_gate.failed` line in this repo's ledger at all — the
555
+ // gate failure was, by construction, the one event that could never be recorded.
556
+ //
557
+ // `--ledger-root` is that hook's channel for handing back the root it already
558
+ // knows (`git rev-parse --show-toplevel`, read BEFORE it cds). It pins ONLY the
559
+ // telemetry destination and the attribution root — every other path the gate
560
+ // resolves stays inside the tree actually under test, which is the whole point
561
+ // of the materialisation. Absent (every other caller) → unchanged behaviour:
562
+ // `emitEvent`'s own default resolution.
563
+ //
564
+ // It is an ARGV FLAG and not an env var, and that is load-bearing. Measured
565
+ // 2026-09-06 with the env-var form: `SO_GATE_LEDGER_ROOT=$tmp npx vitest run
566
+ // tests/scripts/run-quality-gate.test.mjs -t "telemetry emission"` → `8 failed |
567
+ // 1 passed`. The chain was: hook exports the var → `npm run quality-gate` →
568
+ // `gate-full.mjs` spawns `npm test` → every vitest worker inherits it → the
569
+ // suite's own gate spawns spread `...process.env`, so the pinned root outranked
570
+ // their per-test project dir and the gate's telemetry tests wrote to the hook's
571
+ // root. The gate that releases 4.0.0 would have blocked on itself. An env var is
572
+ // inherited by every descendant; a flag reaches exactly one process.
573
+ //
574
+ // Best-effort: a telemetry failure must NEVER alter the gate's authoritative
575
+ // exit code — which is why the counts parse also lives inside this try.
576
+ const exitCode = outcome.exitCode;
577
+ try {
578
+ const counts = suiteCountsFromGateStdout(gateStdout);
579
+ // The names behind `counts.failed`. Absent, never `[]` — see
580
+ // `failedFilesFromGateStdout`.
581
+ const failedFiles = failedFilesFromGateStdout(gateStdout);
582
+ // Wave-scope sidecar is read from the SAME project dir the event lands in
583
+ // (emitEvent's own destination precedence), so a tmp-scoped run cannot pick
584
+ // up the host repo's live wave. Mirrors the hook's projectDir resolution.
585
+ const waveNumber = resolveWaveNumber(
586
+ ledgerRoot ?? process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot,
587
+ );
588
+ // `timed_out` / `survivors` / `kill_signals`: the OUTER ladder (the gate
589
+ // sub-script as a whole, #1439 — same builder as the library emitter)
590
+ // OR-joined with the INNER per-command ladders the envelope publishes
591
+ // (#1457). Keys omitted when the inner ladder was not measured and the
592
+ // outer one did not fire — see `joinGateKillFields`.
593
+ const killFields = joinGateKillFields({ outer: gateKillFields(result), gateStdout });
594
+ // A killed gate is `failed` in every variant (#1459, owner decision
595
+ // 2026-10-01). The reporting variants (baseline / incremental / per-file)
596
+ // exit 0 even when a command was killed, so the exit code alone would
597
+ // record that run as `passed`. Their exit code stays 0 on purpose: it has
598
+ // never carried the check verdict there (a failing check exits 0 too).
599
+ const failed = exitCode !== 0 || killFields.timed_out === true;
600
+ await emitEvent(
601
+ `orchestrator.quality_gate.${failed ? 'failed' : 'passed'}`,
602
+ {
603
+ variant,
604
+ exit_code: exitCode,
605
+ ...killFields,
606
+ ...(counts ? { counts } : {}),
607
+ ...(failedFiles ? { failed_files: failedFiles } : {}),
608
+ ...(waveNumber !== null ? { wave_number: waveNumber } : {}),
609
+ ...sessionAttribution(ledgerRoot ?? repoRoot),
610
+ },
611
+ // `{}` is byte-identical to omitting the argument (`opts.repoRoot ??
612
+ // getProjectDir()`), so the default path is untouched.
613
+ ledgerRoot ? { repoRoot: ledgerRoot } : {},
614
+ );
615
+ } catch { /* best-effort telemetry — gate result is authoritative */ }
616
+
617
+ process.exit(exitCode);
400
618
  }
401
619
 
402
- // Quality-gate telemetry — one canonical event per gate run via emitEvent
403
- // (single emission path). `sessionAttribution` is the shared helper in
404
- // events.mjs (#941); this CLI wrapper runs against the CWD `repoRoot`, so the
405
- // bare emitEvent destination (SO_PROJECT_DIR default) is correct here.
406
- //
407
- // EXCEPT under the pre-push hook, which is the one caller that runs the gate in
408
- // a tree that is about to be DELETED. `.husky/pre-push` materialises the tracked
409
- // tree into a temp dir and deliberately scrubs every `*PROJECT_DIR` name before
410
- // invoking the gate there, so `getProjectDir()` resolves to that temp tree (it
411
- // carries both a CLAUDE.md (or AGENTS.md) and a .git) and the record lands in
412
- // `<tmp>/.orchestrator/metrics/events.jsonl`, which the hook's EXIT trap then
413
- // removes. Measured 2026-09-06: a pre-push run that BLOCKED a push left no
414
- // `orchestrator.quality_gate.failed` line in this repo's ledger at all — the
415
- // gate failure was, by construction, the one event that could never be recorded.
416
- //
417
- // `--ledger-root` is that hook's channel for handing back the root it already
418
- // knows (`git rev-parse --show-toplevel`, read BEFORE it cds). It pins ONLY the
419
- // telemetry destination and the attribution root — every other path the gate
420
- // resolves stays inside the tree actually under test, which is the whole point
421
- // of the materialisation. Absent (every other caller) → unchanged behaviour:
422
- // `emitEvent`'s own default resolution.
423
- //
424
- // It is an ARGV FLAG and not an env var, and that is load-bearing. Measured
425
- // 2026-09-06 with the env-var form: `SO_GATE_LEDGER_ROOT=$tmp npx vitest run
426
- // tests/scripts/run-quality-gate.test.mjs -t "telemetry emission"` → `8 failed |
427
- // 1 passed`. The chain was: hook exports the var → `npm run quality-gate` →
428
- // `gate-full.mjs` spawns `npm test` → every vitest worker inherits it → the
429
- // suite's own gate spawns spread `...process.env`, so the pinned root outranked
430
- // their per-test project dir and the gate's telemetry tests wrote to the hook's
431
- // root. The gate that releases 4.0.0 would have blocked on itself. An env var is
432
- // inherited by every descendant; a flag reaches exactly one process.
433
- //
434
- // Best-effort: a telemetry failure must NEVER alter the gate's authoritative
435
- // exit code — which is why the counts parse also lives inside this try.
436
- const exitCode = result.status ?? 1;
437
- const ledgerRoot = resolveLedgerRoot(ledgerRootArg);
438
- try {
439
- const counts = suiteCountsFromGateStdout(gateStdout);
440
- // The names behind `counts.failed`. Absent, never `[]` — see
441
- // `failedFilesFromGateStdout`.
442
- const failedFiles = failedFilesFromGateStdout(gateStdout);
443
- // Wave-scope sidecar is read from the SAME project dir the event lands in
444
- // (emitEvent's own destination precedence), so a tmp-scoped run cannot pick
445
- // up the host repo's live wave. Mirrors the hook's projectDir resolution.
446
- const waveNumber = resolveWaveNumber(
447
- ledgerRoot ?? process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_PROJECT_DIR ?? repoRoot,
448
- );
449
- await emitEvent(
450
- `orchestrator.quality_gate.${exitCode === 0 ? 'passed' : 'failed'}`,
451
- {
452
- variant,
453
- exit_code: exitCode,
454
- ...(counts ? { counts } : {}),
455
- ...(failedFiles ? { failed_files: failedFiles } : {}),
456
- ...(waveNumber !== null ? { wave_number: waveNumber } : {}),
457
- ...sessionAttribution(ledgerRoot ?? repoRoot),
458
- },
459
- // `{}` is byte-identical to omitting the argument (`opts.repoRoot ??
460
- // getProjectDir()`), so the default path is untouched.
461
- ledgerRoot ? { repoRoot: ledgerRoot } : {},
462
- );
463
- } catch { /* best-effort telemetry — gate result is authoritative */ }
464
-
465
- process.exit(exitCode);
620
+ // Entry guard (#1440 C1): run only as the node script `npm run quality-gate` /
621
+ // the pre-push hook exec — a bare `import()` must parse no argv, spawn no gate
622
+ // and never `process.exit` the importing process.
623
+ if (isMainModule(import.meta.url)) {
624
+ await main();
625
+ }