session-orchestrator 3.24.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1125 -2
  81. package/NOTICE +11 -6
  82. package/README.md +127 -94
  83. package/agents/eval-judge.md +1 -1
  84. package/agents/skill-applied-judge.md +1 -1
  85. package/assets/wave-lifecycle.svg +98 -0
  86. package/commands/release.md +6 -3
  87. package/commands/session.md +18 -3
  88. package/docs/README.md +4 -0
  89. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  90. package/docs/baseline.md +67 -0
  91. package/docs/ci-setup.md +108 -62
  92. package/docs/codex-setup.md +65 -21
  93. package/docs/components.md +36 -15
  94. package/docs/cursor-setup.md +6 -2
  95. package/docs/events-schema.md +9 -6
  96. package/docs/instruction-delivery.md +62 -0
  97. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  98. package/docs/migration-v4.md +341 -0
  99. package/docs/pi-setup.md +6 -1
  100. package/docs/plugin-architecture-v3.md +1 -1
  101. package/docs/rule-authoring.md +85 -19
  102. package/docs/scope-collision-guard.md +5 -5
  103. package/docs/session-config-reference.md +57 -56
  104. package/docs/session-config-template.md +6 -29
  105. package/docs/telemetry.md +157 -3
  106. package/docs/vault-docs-architecture.md +50 -11
  107. package/hooks/_lib/hook-import-set.json +1487 -0
  108. package/hooks/_lib/subagent-transcript.mjs +562 -0
  109. package/hooks/config-protection.mjs +2 -2
  110. package/hooks/cwd-change-restore.mjs +2 -2
  111. package/hooks/enforce-commands.mjs +69 -0
  112. package/hooks/hooks-codex.json +1 -1
  113. package/hooks/hooks-cursor.json +10 -0
  114. package/hooks/hooks-pi.json +5 -0
  115. package/hooks/hooks.json +6 -1
  116. package/hooks/loop-guard.mjs +3 -3
  117. package/hooks/on-session-end.mjs +2 -2
  118. package/hooks/on-session-start.mjs +103 -2
  119. package/hooks/on-stop.mjs +36 -11
  120. package/hooks/operator-steer.mjs +2 -2
  121. package/hooks/post-bash-write-verify.mjs +85 -0
  122. package/hooks/post-edit-import-probe.mjs +344 -0
  123. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  124. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  125. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  126. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  127. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  128. package/hooks/skill-invocation-telemetry.mjs +17 -5
  129. package/hooks/subagent-telemetry.mjs +13 -4
  130. package/monitors/monitors.json +3 -3
  131. package/package.json +9 -1
  132. package/pi/prompts/session.md +2 -2
  133. package/plugin.json +27 -0
  134. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  135. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  136. package/scripts/dialectic-deriver.mjs +73 -8
  137. package/scripts/export-hw-learnings.mjs +113 -1
  138. package/scripts/generate-agents-skills.mjs +378 -0
  139. package/scripts/generate-cursor-adapter.mjs +45 -8
  140. package/scripts/generate-hook-import-set.mjs +249 -0
  141. package/scripts/lib/agent-status.mjs +13 -2
  142. package/scripts/lib/auto-dream.mjs +38 -36
  143. package/scripts/lib/autonomy/suitability.mjs +6 -0
  144. package/scripts/lib/autopilot/loop.mjs +2 -2
  145. package/scripts/lib/ci-status-banner.mjs +220 -75
  146. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  147. package/scripts/lib/config/auto-dream.mjs +2 -1
  148. package/scripts/lib/config/block-header.mjs +8 -0
  149. package/scripts/lib/config/block-preprocess.mjs +177 -0
  150. package/scripts/lib/config/broken-window.mjs +2 -1
  151. package/scripts/lib/config/cold-start.mjs +2 -1
  152. package/scripts/lib/config/config-protection.mjs +22 -2
  153. package/scripts/lib/config/context-coverage.mjs +2 -1
  154. package/scripts/lib/config/cross-repo.mjs +2 -1
  155. package/scripts/lib/config/custom-phases.mjs +2 -1
  156. package/scripts/lib/config/dialectic.mjs +2 -1
  157. package/scripts/lib/config/discovery-validator.mjs +2 -1
  158. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  159. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  160. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  161. package/scripts/lib/config/docs-staleness.mjs +2 -1
  162. package/scripts/lib/config/drift-check.mjs +2 -1
  163. package/scripts/lib/config/eval.mjs +2 -1
  164. package/scripts/lib/config/events-rotation.mjs +2 -1
  165. package/scripts/lib/config/evolve.mjs +8 -2
  166. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  167. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  168. package/scripts/lib/config/handover-gate.mjs +2 -1
  169. package/scripts/lib/config/health-endpoints.mjs +7 -2
  170. package/scripts/lib/config/issue-budget.mjs +2 -1
  171. package/scripts/lib/config/loop-guard.mjs +2 -1
  172. package/scripts/lib/config/memory.mjs +2 -1
  173. package/scripts/lib/config/moc-staleness.mjs +2 -1
  174. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  175. package/scripts/lib/config/private-config-dir.mjs +67 -0
  176. package/scripts/lib/config/reconcile.mjs +2 -1
  177. package/scripts/lib/config/remote-hosts.mjs +2 -1
  178. package/scripts/lib/config/section-extractor.mjs +7 -1
  179. package/scripts/lib/config/skill-evolution.mjs +2 -1
  180. package/scripts/lib/config/slopcheck.mjs +2 -1
  181. package/scripts/lib/config/state-md-lock.mjs +2 -1
  182. package/scripts/lib/config/templates-first.mjs +2 -1
  183. package/scripts/lib/config/test.mjs +2 -1
  184. package/scripts/lib/config/vault-integration.mjs +7 -1
  185. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  186. package/scripts/lib/config/vault-staleness.mjs +2 -1
  187. package/scripts/lib/config/vault-sync.mjs +2 -1
  188. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  189. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  190. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  191. package/scripts/lib/convergence-monitor.mjs +82 -16
  192. package/scripts/lib/dispatcher/rank.mjs +124 -48
  193. package/scripts/lib/ecosystem-health.mjs +16 -2
  194. package/scripts/lib/eval/engine.mjs +9 -1
  195. package/scripts/lib/eval/session-resolve.mjs +23 -4
  196. package/scripts/lib/events.mjs +22 -6
  197. package/scripts/lib/frontmatter-guard.mjs +131 -13
  198. package/scripts/lib/gates/gate-full.mjs +26 -0
  199. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  200. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  201. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  202. package/scripts/lib/host-identity.mjs +50 -11
  203. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  204. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  205. package/scripts/lib/learnings/io.mjs +60 -6
  206. package/scripts/lib/memory-proposals/store.mjs +30 -22
  207. package/scripts/lib/owner-config-banner.mjs +43 -6
  208. package/scripts/lib/owner-config-loader.mjs +21 -10
  209. package/scripts/lib/owner-interview.mjs +3 -3
  210. package/scripts/lib/owner-yaml.mjs +207 -14
  211. package/scripts/lib/platform.mjs +108 -15
  212. package/scripts/lib/plugin-update-banner.mjs +406 -0
  213. package/scripts/lib/project-hygiene.mjs +38 -2
  214. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  215. package/scripts/lib/quality-gate.mjs +133 -44
  216. package/scripts/lib/reconcile/emitter.mjs +68 -6
  217. package/scripts/lib/reconcile/engine.mjs +13 -4
  218. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  219. package/scripts/lib/reconcile/writer.mjs +40 -18
  220. package/scripts/lib/session-close-backfill.mjs +67 -9
  221. package/scripts/lib/session-id.mjs +12 -23
  222. package/scripts/lib/session-identity/own-session.mjs +125 -10
  223. package/scripts/lib/session-lock-shape.mjs +43 -0
  224. package/scripts/lib/session-lock.mjs +5 -10
  225. package/scripts/lib/session-registry.mjs +25 -9
  226. package/scripts/lib/session-schema/constants.mjs +36 -2
  227. package/scripts/lib/session-schema/validator.mjs +38 -4
  228. package/scripts/lib/session-start-probes.mjs +18 -1
  229. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  230. package/scripts/lib/skill-health/join.mjs +17 -4
  231. package/scripts/lib/state-md.mjs +78 -0
  232. package/scripts/lib/sunset/walker.mjs +6 -0
  233. package/scripts/lib/telemetry/schema.mjs +181 -9
  234. package/scripts/lib/telemetry/sync.mjs +368 -12
  235. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  236. package/scripts/lib/validate/check-agents.mjs +3 -3
  237. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  238. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  239. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  240. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  241. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  242. package/scripts/lib/validate/check-unwired-features.mjs +0 -2
  243. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  244. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  245. package/scripts/lib/vault-backfill/template.mjs +63 -6
  246. package/scripts/lib/vault-mirror/process.mjs +165 -42
  247. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  248. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  249. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  250. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  251. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  252. package/scripts/lib/wave-resource-gate.mjs +8 -2
  253. package/scripts/lib/wave-sizing.mjs +4 -1
  254. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  255. package/scripts/materialize-wave-scope.mjs +12 -5
  256. package/scripts/memory-propose.mjs +19 -5
  257. package/scripts/migrate-cold-start-seed.mjs +4 -1
  258. package/scripts/parse-config.mjs +60 -3
  259. package/scripts/release.mjs +337 -29
  260. package/scripts/repair-invalid-sessions.mjs +3 -3
  261. package/scripts/run-quality-gate.mjs +128 -11
  262. package/scripts/sweep-expired-learnings.mjs +90 -0
  263. package/scripts/sync-vault-schema.mjs +3 -1
  264. package/scripts/telemetry.mjs +2 -2
  265. package/scripts/validate-plugin.mjs +161 -0
  266. package/scripts/validate-wave-scope.mjs +28 -8
  267. package/scripts/wave-scope-binding.mjs +215 -0
  268. package/skills/_shared/instruction-file-resolution.md +10 -0
  269. package/skills/_shared/parallel-aware-preamble.md +1 -0
  270. package/skills/_shared/platform-tools.md +1 -1
  271. package/skills/_shared/state-ownership.md +1 -1
  272. package/skills/architecture/SKILL.md +7 -5
  273. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  274. package/skills/autopilot/SKILL.md +4 -18
  275. package/skills/claude-md-drift-check/SKILL.md +5 -1
  276. package/skills/claude-md-drift-check/checker.mjs +62 -2
  277. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  278. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  279. package/skills/discovery/probes-arch.md +20 -18
  280. package/skills/dispatcher/SKILL.md +3 -2
  281. package/skills/evolve/SKILL.md +65 -26
  282. package/skills/frontmatter-guard/SKILL.md +11 -5
  283. package/skills/npm-publish/SKILL.md +1 -1
  284. package/skills/reconcile/SKILL.md +33 -0
  285. package/skills/remote-offload/SKILL.md +1 -1
  286. package/skills/session-end/SKILL.md +18 -905
  287. package/skills/session-end/phase-3-6-tail.md +10 -3
  288. package/skills/session-end/plan-verification.md +221 -155
  289. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  290. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  291. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  292. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  293. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  294. package/skills/session-end/references/session-summary-template.md +62 -0
  295. package/skills/session-plan/SKILL.md +49 -0
  296. package/skills/session-start/SKILL.md +22 -904
  297. package/skills/session-start/phase-8-5-express-path.md +1 -1
  298. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  299. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  300. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  301. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  302. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  303. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  304. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  305. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  306. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  307. package/skills/vault-sync/validator.mjs +21 -27
  308. package/skills/wave-executor/SKILL.md +15 -1
  309. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  310. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  311. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  312. package/skills/wave-executor/wave-loop.md +14 -1309
  313. package/templates/_shared/journey-manifest.md +10 -6
  314. package/.cursor/commands/autopilot-multi.md +0 -14
  315. package/.cursor/commands/contract-version-bump.md +0 -14
  316. package/.cursor/commands/journey-audit.md +0 -14
  317. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  318. package/.cursor/skills/daily/SKILL.md +0 -12
  319. package/.cursor/skills/domain-model/SKILL.md +0 -13
  320. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  321. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  322. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  323. package/commands/autopilot-multi.md +0 -74
  324. package/commands/contract-version-bump.md +0 -28
  325. package/commands/journey-audit.md +0 -43
  326. package/pi/prompts/autopilot-multi.md +0 -12
  327. package/pi/prompts/contract-version-bump.md +0 -12
  328. package/pi/prompts/journey-audit.md +0 -12
  329. package/scripts/autopilot-multi.mjs +0 -885
  330. package/scripts/backfill-learnings-expires.mjs +0 -196
  331. package/scripts/backfill-learnings.mjs +0 -203
  332. package/scripts/fleet-instruction-scan.mjs +0 -141
  333. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  334. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  335. package/scripts/lib/webhook-url.mjs +0 -105
  336. package/scripts/lifecycle-sim-v6.mjs +0 -347
  337. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  338. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  339. package/scripts/upload-social-preview.mjs +0 -316
  340. package/skills/_shared/model-selection.md +0 -64
  341. package/skills/contract-version-bump/SKILL.md +0 -219
  342. package/skills/daily/SKILL.md +0 -222
  343. package/skills/daily/generate.sh +0 -92
  344. package/skills/daily/templates/daily.md.tpl +0 -36
  345. package/skills/journey-audit/SKILL.md +0 -270
  346. package/skills/skill-creator/SKILL.md +0 -168
  347. package/skills/ubiquitous-language/SKILL.md +0 -97
  348. package/skills/vault-sync/package-lock.json +0 -40
  349. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  350. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * moc-staleness.mjs — Parser for the top-level `moc-staleness:` YAML block.
@@ -31,7 +32,7 @@ export function _parseMocStaleness(content) {
31
32
  mode: 'warn',
32
33
  };
33
34
 
34
- const lines = content.split(/\r?\n/);
35
+ const lines = preprocessBlockLines(content);
35
36
  let inBlock = false;
36
37
  const blockLines = [];
37
38
 
@@ -20,6 +20,7 @@
20
20
  import { parseThreshold } from '../persona-panel/threshold.mjs';
21
21
  import { ALLOWED_MODEL_ALIASES, MODEL_ID_RE } from '../agent-frontmatter.mjs';
22
22
  import { matchBlockHeader } from './block-header.mjs';
23
+ import { preprocessBlockLines } from './block-preprocess.mjs';
23
24
 
24
25
  const ALLOWED_AFTER = new Set(['quality', 'impl-polish']);
25
26
  const ALLOWED_MODE = new Set(['off', 'warn', 'strict']);
@@ -169,7 +170,7 @@ export function _normalizePersonaGateWave(parsed) {
169
170
  * @returns {string[]}
170
171
  */
171
172
  function _extractBlock(content, key) {
172
- const lines = content.split(/\r?\n/);
173
+ const lines = preprocessBlockLines(content);
173
174
  const blockLines = [];
174
175
  let inBlock = false;
175
176
 
@@ -0,0 +1,67 @@
1
+ /**
2
+ * private-config-dir.mjs — THE resolver for the host-private config directory (#1223).
3
+ *
4
+ * Every per-host artefact below that directory (`host-private.json`,
5
+ * `owner.yaml`, the self-alias ledger) moves with it, so the precedence has to
6
+ * live in exactly ONE place. Before #1223 three copies with three different
7
+ * precedences existed: `owner-yaml.mjs`'s import-time homedir-only
8
+ * `OWNER_YAML_PATH`, `owner-config-loader.mjs`'s XDG-only + untrimmed
9
+ * `resolveOwnerConfigPath()`, and `host-identity.mjs`'s `_privateDir()`. The
10
+ * live consequence: `SO_CONFIG_HOME=<sandbox>` moved the alias ledger but NOT
11
+ * owner.yaml, which kept being read from the operator's REAL home — the
12
+ * CLAUDE.md (Codex CLI alias: AGENTS.md) "vault-dir resolves HOST-LOCALLY"
13
+ * hazard class.
14
+ *
15
+ * ── Why this is a standalone ZERO-IMPORT leaf ────────────────────────────────
16
+ *
17
+ * Two independent consumers constrain it, and neither tolerates the other's
18
+ * import graph:
19
+ *
20
+ * 1. `host-identity.mjs` is reachable from LIVE hooks (via `session-lock.mjs`),
21
+ * `hooks/on-stop.mjs` among them. on-stop must degrade with one actionable
22
+ * line when `node_modules` is absent (GH#63), so nothing on ITS subgraph —
23
+ * this leaf included — may pull a bare specifier. Putting the resolver in
24
+ * `owner-yaml.mjs` dragged `js-yaml` onto that subgraph and turned the GH#63
25
+ * contract red with `ERR_MODULE_NOT_FOUND: Cannot find package 'js-yaml'`.
26
+ * Since GitLab #1230 (2026-09-06) `owner-yaml.mjs` resolves `js-yaml`
27
+ * lazily, so that particular collision can no longer recur — but the
28
+ * constraint on THIS file is unchanged, because it is what let the resolver
29
+ * be shared in the first place. Measured 2026-09-06 over a `hooks/` +
30
+ * `scripts/` copy with no `node_modules`, the four hooks that reach
31
+ * `owner-yaml.mjs` (on-session-start, on-session-end, post-edit-validate,
32
+ * skill-invocation-telemetry) now load instead of throwing.
33
+ * 2. `tests/husky/pre-commit-owner-leakage.test.mjs` copies the CP11 scanner's
34
+ * import chain FILE BY FILE into a tmp repo. Every repo-local import added
35
+ * to a file on that chain must be added to the copied-file list there.
36
+ *
37
+ * Hence: `node:os` + `node:path` only. No repo-local imports, no bare
38
+ * specifiers beyond the two Node builtins.
39
+ */
40
+
41
+ import { homedir } from 'node:os';
42
+ import { join } from 'node:path';
43
+
44
+ /**
45
+ * Resolve the host-private config directory.
46
+ *
47
+ * Precedence, most specific first: `SO_CONFIG_HOME` names the private dir
48
+ * ITSELF; `XDG_CONFIG_HOME` names its PARENT (the `session-orchestrator`
49
+ * segment is appended); without either, `~/.config/session-orchestrator`.
50
+ *
51
+ * `.trim() || fallback` throughout — a whitespace-only env var is truthy and
52
+ * would short-circuit a bare `||`, returning the spaces verbatim
53
+ * (`.claude/rules/development.md` § Error Handling, env-var fallback
54
+ * whitespace trap).
55
+ *
56
+ * Reads env at CALL time, so a test may stub `process.env` after import.
57
+ *
58
+ * @param {{ env?: Record<string, string|undefined> }} [opts]
59
+ * @returns {string} absolute path to the host-private config directory
60
+ */
61
+ export function resolvePrivateConfigDir({ env = process.env } = {}) {
62
+ const own = (env.SO_CONFIG_HOME || '').trim();
63
+ if (own) return own;
64
+ const xdg = (env.XDG_CONFIG_HOME || '').trim();
65
+ if (xdg) return join(xdg, 'session-orchestrator');
66
+ return join(homedir(), '.config', 'session-orchestrator');
67
+ }
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * reconcile.mjs — Parser for the top-level `reconcile:` YAML block.
@@ -169,7 +170,7 @@ export function _parseReconcile(content) {
169
170
  'max-proposals-per-run': 10,
170
171
  };
171
172
 
172
- const lines = content.split(/\r?\n/);
173
+ const lines = preprocessBlockLines(content);
173
174
  let inBlock = false;
174
175
  const blockLines = [];
175
176
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLinesNoDash } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * remote-hosts.mjs — Parser for the `remote-hosts:` Session Config block (#1160).
@@ -73,7 +74,7 @@ const SAFE_PATH_RE = /^[A-Za-z0-9._~/][A-Za-z0-9._~/-]*$/;
73
74
  * @returns {Array<{alias: string, 'roles-allowed': string[], 'repo-path': string|null, 'claude-path': string|null}>}
74
75
  */
75
76
  export function _parseRemoteHosts(content) {
76
- const lines = String(content ?? '').split(/\r?\n/);
77
+ const lines = preprocessBlockLinesNoDash(String(content ?? ''));
77
78
  let inBlock = false;
78
79
  const blockLines = [];
79
80
 
@@ -214,10 +214,16 @@ function isCodeFence(line) {
214
214
  * this repo's own convention encourages. Revisit if a fleet CLAUDE.md is ever
215
215
  * measured opening a multi-line comment after a value.
216
216
  *
217
+ * EXPORTED since #1162 for a third consumer: `block-preprocess.mjs`, which
218
+ * gives the ~36 block parsers under `scripts/lib/config/` the same
219
+ * commented-out-is-not-live-config semantics. Same argument as the export of
220
+ * `isSessionConfigHeading` — a fourth copy of this state machine that drifts
221
+ * loose is worse than no check at all. Behaviour is unchanged by the export.
222
+ *
217
223
  * @returns {(line: string) => boolean} true when the line is inside (or opens)
218
224
  * a multi-line HTML comment and must be ignored by both consumers.
219
225
  */
220
- function htmlCommentSkipper() {
226
+ export function htmlCommentSkipper() {
221
227
  let inComment = false;
222
228
  return function skipHtmlComment(line) {
223
229
  if (inComment) {
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * skill-evolution.mjs — Parser for the top-level `skill-evolution:` YAML block
@@ -39,7 +40,7 @@ export function _parseSkillEvolution(content) {
39
40
  'judge-budget-tokens': 8000,
40
41
  };
41
42
 
42
- const lines = content.split(/\r?\n/);
43
+ const lines = preprocessBlockLines(content);
43
44
  let inBlock = false;
44
45
  const blockLines = [];
45
46
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * slopcheck.mjs — Parser for the top-level `slopcheck:` YAML block
@@ -36,7 +37,7 @@ export function _parseSlopcheck(content) {
36
37
  sources: [...DEFAULT_SOURCES],
37
38
  };
38
39
 
39
- const lines = content.split(/\r?\n/);
40
+ const lines = preprocessBlockLines(content);
40
41
  let inBlock = false;
41
42
  const blockLines = [];
42
43
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * state-md-lock.mjs — Parser for the top-level `state-md-lock:` YAML block
@@ -30,7 +31,7 @@ export function _parseStateMdLock(content) {
30
31
  'timeout-ms': 10000,
31
32
  };
32
33
 
33
- const lines = content.split(/\r?\n/);
34
+ const lines = preprocessBlockLines(content);
34
35
  let inBlock = false;
35
36
  const blockLines = [];
36
37
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * templates-first.mjs — Parser for the top-level `templates-first:` YAML block
@@ -34,7 +35,7 @@ export function _parseTemplatesFirst(content) {
34
35
  hosts: [...DEFAULT_HOSTS],
35
36
  };
36
37
 
37
- const lines = content.split(/\r?\n/);
38
+ const lines = preprocessBlockLines(content);
38
39
  let inBlock = false;
39
40
  const blockLines = [];
40
41
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { validatePathInsideProject } from '../path-utils.mjs';
10
10
  import { matchBlockHeader } from './block-header.mjs';
11
+ import { preprocessBlockLines } from './block-preprocess.mjs';
11
12
 
12
13
  /**
13
14
  * Parse the top-level `test:` YAML block from markdown content.
@@ -27,7 +28,7 @@ export function _parseTest(content) {
27
28
  'retention-days': 30,
28
29
  };
29
30
 
30
- const lines = content.split(/\r?\n/);
31
+ const lines = preprocessBlockLines(content);
31
32
  let inBlock = false;
32
33
  const blockLines = [];
33
34
 
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { _coerceBoolean, _coerceInteger } from './coercers.mjs';
24
24
  import { matchBlockHeader } from './block-header.mjs';
25
+ import { preprocessBlockLines } from './block-preprocess.mjs';
25
26
 
26
27
  // ---------------------------------------------------------------------------
27
28
  // vault-integration
@@ -68,7 +69,12 @@ export function _parseVaultIntegration(content) {
68
69
  const defaults = { enabled: false, 'vault-dir': null, mode: 'warn', 'vault-name': null };
69
70
  if (typeof content !== 'string' || content === '') return defaults;
70
71
 
71
- const lines = content.split(/\r?\n/);
72
+ // #1162: NOT a raw split. `preprocessBlockLines` drops HTML-commented lines
73
+ // (a commented-out block was read as live config) and rewrites bold-bullet
74
+ // sub-keys (`- **enabled:** true` fell back to the `false` default). This
75
+ // parser has no dash-RECORD body, so the bold pass is safe here — see the
76
+ // named risk in block-preprocess.mjs.
77
+ const lines = preprocessBlockLines(content);
72
78
 
73
79
  // Pass 1: inline-object form. Matches `vault-integration: { ... }` with or
74
80
  // without a leading `- ` (baseline list-item form per #497).
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * vault-mirror-quality.mjs — Parser for the top-level `vault-mirror:` YAML block,
@@ -30,7 +31,7 @@ export function _parseVaultMirrorQuality(content) {
30
31
  },
31
32
  };
32
33
 
33
- const lines = content.split(/\r?\n/);
34
+ const lines = preprocessBlockLines(content);
34
35
  let inBlock = false;
35
36
  const blockLines = [];
36
37
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * vault-staleness.mjs — Parser for the top-level `vault-staleness:` YAML block.
@@ -17,7 +18,7 @@ export function _parseVaultStaleness(content) {
17
18
  mode: 'warn',
18
19
  };
19
20
 
20
- const lines = content.split(/\r?\n/);
21
+ const lines = preprocessBlockLines(content);
21
22
  let inBlock = false;
22
23
  const blockLines = [];
23
24
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * vault-sync.mjs — Parser for the top-level `vault-sync:` YAML block.
@@ -16,7 +17,7 @@ import { matchBlockHeader } from './block-header.mjs';
16
17
  export function _parseVaultSync(content) {
17
18
  const defaults = { enabled: false, mode: 'warn', 'vault-dir': null, exclude: [] };
18
19
 
19
- const lines = content.split(/\r?\n/);
20
+ const lines = preprocessBlockLines(content);
20
21
  let inBlock = false;
21
22
  const blockLines = [];
22
23
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * verification-auto-fix.mjs — Parser for the top-level `verification-auto-fix:`
@@ -31,7 +32,7 @@ export function _parseVerificationAutoFix(content) {
31
32
  'max-retries': 2,
32
33
  };
33
34
 
34
- const lines = content.split(/\r?\n/);
35
+ const lines = preprocessBlockLines(content);
35
36
  let inBlock = false;
36
37
  const blockLines = [];
37
38
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * wave-reviewers.mjs — Parser for the `wave-reviewers` / `persona-reviewers` sub-block.
@@ -58,7 +59,7 @@ export function _parseWaveReviewers(content) {
58
59
  * @returns {string[]}
59
60
  */
60
61
  function _extractBlock(content, key) {
61
- const lines = content.split(/\r?\n/);
62
+ const lines = preprocessBlockLines(content);
62
63
  const blockLines = [];
63
64
  let inBlock = false;
64
65
 
@@ -1,4 +1,5 @@
1
1
  import { matchBlockHeader } from './block-header.mjs';
2
+ import { preprocessBlockLines } from './block-preprocess.mjs';
2
3
 
3
4
  /**
4
5
  * worktree-orphans.mjs — Parser for the top-level `worktree-orphans:` YAML block.
@@ -85,7 +86,7 @@ export function _parseWorktreeOrphans(content) {
85
86
 
86
87
  if (typeof content !== 'string' || content === '') return defaults;
87
88
 
88
- const lines = content.split(/\r?\n/);
89
+ const lines = preprocessBlockLines(content);
89
90
  let inBlock = false;
90
91
  const blockLines = [];
91
92
 
@@ -39,6 +39,7 @@
39
39
  * @property {number} waveNumber
40
40
  * @property {number | null} filesChanged
41
41
  * @property {number | null} testPassed
42
+ * @property {number | null} testFailed
42
43
  * @property {number | null} agentDispatchCount
43
44
  */
44
45
 
@@ -143,21 +144,56 @@ function tailRead(absPath, prevOffset) {
143
144
  * Event types this monitor is meant to classify. Anything else is ignored even
144
145
  * when it carries a wave number.
145
146
  *
146
- * The allowlist is a wave-LIFECYCLE prefix plus the agent-dispatch counters —
147
- * i.e. exactly the records that can carry the three measurements
148
- * `evaluateSignals` compares (`files_changed`, `test.passed`,
149
- * `agents_dispatched`). It is deliberately fail-CLOSED: a new event type that
150
- * gains a `wave_number` field must be added here consciously.
147
+ * The allowlist is a wave-LIFECYCLE prefix, the agent-dispatch/-stop counters,
148
+ * and the quality-gate envelope — i.e. exactly the records that can carry the
149
+ * three measurements `evaluateSignals` compares (`files_changed`, the test
150
+ * pass count, the per-wave agent count). It is deliberately fail-CLOSED: a new
151
+ * event type that gains a `wave_number` field must be added here consciously.
152
+ *
153
+ * The gate admission is a TYPE-AND-SHAPE gate, never a bare prefix widening —
154
+ * see `isWaveScopedEvent` for why.
151
155
  */
152
156
  const WAVE_EVENT_PREFIX = 'orchestrator.wave.';
153
157
  const WAVE_EVENT_NAMES = new Set(['agent.dispatched', 'orchestrator.agent.dispatched']);
158
+ const AGENT_STOPPED_EVENT = 'orchestrator.agent.stopped';
159
+ const GATE_EVENT_PREFIX = 'orchestrator.quality_gate.';
160
+
161
+ /**
162
+ * True when a record carries a `counts` object this monitor can fold, i.e. a
163
+ * plain object with a numeric `passed`. `counts: null`, an array, or a string
164
+ * payload are all rejected — the shape half of the type-and-shape gate.
165
+ *
166
+ * @param {Record<string, unknown>} rec
167
+ * @returns {boolean}
168
+ */
169
+ function hasFoldableCounts(rec) {
170
+ const counts = rec.counts;
171
+ if (counts === null || typeof counts !== 'object' || Array.isArray(counts)) return false;
172
+ return pickInt(/** @type {Record<string, unknown>} */ (counts).passed) !== null;
173
+ }
154
174
 
155
175
  /**
176
+ * Type-AND-shape gate: admit a record only when its event type is one this
177
+ * monitor classifies AND the record actually carries a measurement `classify`
178
+ * can fold. A bare prefix widening for `orchestrator.quality_gate.*` would
179
+ * re-open the #966 trap — the gate is the single highest-volume record type in
180
+ * `events.jsonl`, and admitting the session-level runs (no `wave_number`, or an
181
+ * envelope without `counts`) would instantiate a `WaveSummary` per gate run,
182
+ * advance `latestWave`, and burn the once-per-wave `alreadyEmitted` keys before
183
+ * the real wave record arrives. Hence the discriminator: a gate record is
184
+ * wave-scoped only when it has BOTH `wave_number` and a well-formed `counts`.
185
+ *
156
186
  * @param {string} evType
187
+ * @param {Record<string, unknown>} rec
157
188
  * @returns {boolean}
158
189
  */
159
- function isWaveScopedEvent(evType) {
160
- return evType.startsWith(WAVE_EVENT_PREFIX) || WAVE_EVENT_NAMES.has(evType);
190
+ function isWaveScopedEvent(evType, rec = {}) {
191
+ if (evType.startsWith(WAVE_EVENT_PREFIX)) return true;
192
+ if (WAVE_EVENT_NAMES.has(evType) || evType === AGENT_STOPPED_EVENT) return true;
193
+ if (evType.startsWith(GATE_EVENT_PREFIX)) {
194
+ return pickInt(rec.wave_number) !== null && hasFoldableCounts(rec);
195
+ }
196
+ return false;
161
197
  }
162
198
 
163
199
  /**
@@ -179,12 +215,29 @@ function isWaveScopedEvent(evType) {
179
215
  * `wave_number`, every gate run would have instantiated or refreshed a
180
216
  * `WaveSummary`, advanced `latestWave`, and burnt the once-per-wave
181
217
  * `alreadyEmitted` keys — suppressing the genuine signal when the real wave
182
- * record arrived later. Several other high-volume types (`session.stopped`,
183
- * `memory.propose_invoked`) already carry a wave and sat in the same trap.
218
+ * record arrived later. Several other high-volume types (`turn.stopped` — and its
219
+ * deprecated alias `session.stopped`, GitLab #1234 — plus `memory.propose_invoked`)
220
+ * already carry a wave and sat in the same trap. The gate is an ALLOWLIST
221
+ * (`isWaveRelevantType`), so the rename needed no edit here: a new name is
222
+ * ignored by default, which is the correct default for this classifier.
184
223
  *
185
- * Note the measurement keys are read FLAT (`rec['test.passed']`) and are
186
- * deliberately NOT reconciled with the gate event's nested `counts.passed` —
187
- * folding one into the other would silently change what this monitor measures.
224
+ * ## Where the pass count actually comes from (#980)
225
+ *
226
+ * Two independent paths, deliberately not merged:
227
+ * - the FLAT `rec['test.passed']` alias, kept for a future direct emitter on
228
+ * a wave-lifecycle record — no producer writes it today;
229
+ * - the gate envelope's NESTED `counts.passed`, folded ONLY for
230
+ * `orchestrator.quality_gate.*` records that also carry `wave_number`.
231
+ * That gating is what keeps the #966 invariant intact: an ungated record
232
+ * (a `wave.completed` carrying `counts`, or a session-level gate run with
233
+ * no `wave_number`) still folds NOTHING, so a gate run cannot manufacture a
234
+ * `WaveSummary` and burn the once-per-wave emit keys.
235
+ *
236
+ * `orchestrator.agent.stopped` is counted per record toward the wave's agent
237
+ * count, the same way `agent.dispatched` is — it is the only per-agent record
238
+ * this repo actually emits with a wave number (11,754 records measured
239
+ * 2026-09-05 in `.orchestrator/metrics/events.jsonl`, against 0 for
240
+ * `agent.dispatched`).
188
241
  *
189
242
  * @param {Record<string, unknown>} rec
190
243
  * @param {Map<number, WaveSummary>} state
@@ -192,7 +245,7 @@ function isWaveScopedEvent(evType) {
192
245
  */
193
246
  function classify(rec, state) {
194
247
  const evType = String(rec.event_type ?? rec.event ?? '');
195
- if (!isWaveScopedEvent(evType)) return null;
248
+ if (!isWaveScopedEvent(evType, rec)) return null;
196
249
 
197
250
  const waveNumber = pickInt(rec.wave_number ?? rec.wave ?? rec.waveId);
198
251
  if (waveNumber === null) return null;
@@ -202,6 +255,7 @@ function classify(rec, state) {
202
255
  waveNumber,
203
256
  filesChanged: null,
204
257
  testPassed: null,
258
+ testFailed: null,
205
259
  agentDispatchCount: null,
206
260
  };
207
261
  state.set(waveNumber, summary);
@@ -210,8 +264,17 @@ function classify(rec, state) {
210
264
  if (filesChanged !== null) summary.filesChanged = filesChanged;
211
265
  const testPassed = pickInt(rec['test.passed'] ?? rec.test_passed ?? rec.testsPassed);
212
266
  if (testPassed !== null) summary.testPassed = testPassed;
213
- // Count one agent.dispatched event toward this wave's dispatch count.
214
- if (WAVE_EVENT_NAMES.has(evType)) {
267
+ // Nested gate envelope — admitted by isWaveScopedEvent only WITH wave_number,
268
+ // so the ungated #966 invariant (no fold) survives for every other record.
269
+ if (evType.startsWith(GATE_EVENT_PREFIX)) {
270
+ const counts = /** @type {Record<string, unknown>} */ (rec.counts);
271
+ const gatePassed = pickInt(counts.passed);
272
+ if (gatePassed !== null) summary.testPassed = gatePassed;
273
+ const gateFailed = pickInt(counts.failed);
274
+ if (gateFailed !== null) summary.testFailed = gateFailed;
275
+ }
276
+ // Count one dispatched/stopped agent record toward this wave's agent count.
277
+ if (WAVE_EVENT_NAMES.has(evType) || evType === AGENT_STOPPED_EVENT) {
215
278
  summary.agentDispatchCount = (summary.agentDispatchCount ?? 0) + 1;
216
279
  } else {
217
280
  const dispatched = pickInt(rec.agents_dispatched ?? rec.agentsDispatched);
@@ -443,4 +506,7 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.a
443
506
  main();
444
507
  }
445
508
 
446
- export { classify, isWaveScopedEvent };
509
+ // `_evaluateSignals` is a test-only export (leading `_` per the repo convention,
510
+ // cf. `_clearCompileCache` in scripts/lib/agent-output-schema.mjs): the signal
511
+ // firing is otherwise reachable only through the live tail loop.
512
+ export { classify, isWaveScopedEvent, evaluateSignals as _evaluateSignals };