session-orchestrator 3.23.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1401 -0
  81. package/NOTICE +11 -6
  82. package/README.md +127 -92
  83. package/agents/db-specialist.md +0 -1
  84. package/agents/eval-judge.md +1 -1
  85. package/agents/skill-applied-judge.md +1 -1
  86. package/assets/wave-lifecycle.svg +98 -0
  87. package/commands/release.md +6 -3
  88. package/commands/session.md +18 -3
  89. package/docs/README.md +4 -0
  90. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  91. package/docs/baseline.md +67 -0
  92. package/docs/ci-setup.md +249 -48
  93. package/docs/codex-setup.md +66 -22
  94. package/docs/components.md +37 -16
  95. package/docs/cursor-setup.md +6 -2
  96. package/docs/events-schema.md +51 -10
  97. package/docs/instruction-delivery.md +62 -0
  98. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  99. package/docs/migration-v4.md +341 -0
  100. package/docs/pi-setup.md +6 -1
  101. package/docs/plugin-architecture-v3.md +1 -1
  102. package/docs/rule-authoring.md +85 -19
  103. package/docs/scope-collision-guard.md +8 -8
  104. package/docs/session-config-reference.md +120 -61
  105. package/docs/session-config-template.md +40 -33
  106. package/docs/telemetry/telemetry-claims.md +11 -10
  107. package/docs/telemetry.md +187 -4
  108. package/docs/vault-docs-architecture.md +50 -11
  109. package/hooks/_lib/atomic-json.mjs +111 -0
  110. package/hooks/_lib/hook-import-set.json +1487 -0
  111. package/hooks/_lib/subagent-paths.mjs +143 -0
  112. package/hooks/_lib/subagent-transcript.mjs +562 -0
  113. package/hooks/config-protection.mjs +2 -2
  114. package/hooks/cwd-change-restore.mjs +11 -31
  115. package/hooks/enforce-commands.mjs +69 -0
  116. package/hooks/enforce-scope.mjs +35 -6
  117. package/hooks/hooks-codex.json +1 -1
  118. package/hooks/hooks-cursor.json +10 -0
  119. package/hooks/hooks-pi.json +5 -0
  120. package/hooks/hooks.json +6 -1
  121. package/hooks/loop-guard.mjs +3 -3
  122. package/hooks/on-session-end.mjs +280 -14
  123. package/hooks/on-session-start.mjs +153 -4
  124. package/hooks/on-stop.mjs +371 -17
  125. package/hooks/operator-steer.mjs +2 -2
  126. package/hooks/post-bash-write-verify.mjs +189 -4
  127. package/hooks/post-edit-import-probe.mjs +344 -0
  128. package/hooks/post-subagent-discovery-validator.mjs +278 -392
  129. package/hooks/post-tool-batch-wave-signal.mjs +272 -44
  130. package/hooks/post-tool-failure-corrective-context.mjs +11 -34
  131. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  132. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  133. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  134. package/hooks/skill-invocation-telemetry.mjs +17 -5
  135. package/hooks/subagent-telemetry.mjs +24 -30
  136. package/monitors/monitors.json +3 -3
  137. package/package.json +9 -1
  138. package/pi/prompts/session.md +2 -2
  139. package/plugin.json +27 -0
  140. package/scripts/autopilot.mjs +26 -12
  141. package/scripts/backfill-abandoned-sessions.mjs +130 -15
  142. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  143. package/scripts/dialectic-deriver.mjs +73 -8
  144. package/scripts/emit-event.mjs +10 -2
  145. package/scripts/export-hw-learnings.mjs +113 -1
  146. package/scripts/generate-agents-skills.mjs +378 -0
  147. package/scripts/generate-cursor-adapter.mjs +45 -8
  148. package/scripts/generate-hook-import-set.mjs +249 -0
  149. package/scripts/lib/agent-status.mjs +13 -2
  150. package/scripts/lib/auq/parse.mjs +5 -29
  151. package/scripts/lib/auto-dialectic.mjs +68 -0
  152. package/scripts/lib/auto-dream.mjs +38 -36
  153. package/scripts/lib/autonomy/suitability.mjs +6 -0
  154. package/scripts/lib/autopilot/loop.mjs +2 -2
  155. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  156. package/scripts/lib/build-live-signals.mjs +25 -22
  157. package/scripts/lib/ci-status-banner.mjs +220 -75
  158. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  159. package/scripts/lib/cold-start-detector.mjs +23 -14
  160. package/scripts/lib/config/auto-dream.mjs +2 -1
  161. package/scripts/lib/config/block-header.mjs +63 -0
  162. package/scripts/lib/config/block-preprocess.mjs +177 -0
  163. package/scripts/lib/config/broken-window.mjs +2 -1
  164. package/scripts/lib/config/cold-start.mjs +2 -1
  165. package/scripts/lib/config/config-protection.mjs +22 -2
  166. package/scripts/lib/config/context-coverage.mjs +2 -1
  167. package/scripts/lib/config/cross-repo.mjs +2 -1
  168. package/scripts/lib/config/custom-phases.mjs +2 -1
  169. package/scripts/lib/config/dialectic.mjs +2 -1
  170. package/scripts/lib/config/discovery-validator.mjs +9 -3
  171. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  172. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  173. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  174. package/scripts/lib/config/docs-staleness.mjs +2 -1
  175. package/scripts/lib/config/drift-check.mjs +2 -1
  176. package/scripts/lib/config/eval.mjs +2 -1
  177. package/scripts/lib/config/events-rotation.mjs +2 -1
  178. package/scripts/lib/config/evolve.mjs +8 -2
  179. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  180. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  181. package/scripts/lib/config/handover-gate.mjs +2 -1
  182. package/scripts/lib/config/health-endpoints.mjs +388 -0
  183. package/scripts/lib/config/issue-budget.mjs +2 -1
  184. package/scripts/lib/config/loop-guard.mjs +2 -1
  185. package/scripts/lib/config/memory.mjs +2 -1
  186. package/scripts/lib/config/moc-staleness.mjs +2 -1
  187. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  188. package/scripts/lib/config/private-config-dir.mjs +67 -0
  189. package/scripts/lib/config/reconcile.mjs +2 -1
  190. package/scripts/lib/config/remote-hosts.mjs +234 -0
  191. package/scripts/lib/config/section-extractor.mjs +7 -1
  192. package/scripts/lib/config/skill-evolution.mjs +2 -1
  193. package/scripts/lib/config/slopcheck.mjs +2 -1
  194. package/scripts/lib/config/state-md-lock.mjs +2 -1
  195. package/scripts/lib/config/templates-first.mjs +2 -1
  196. package/scripts/lib/config/test.mjs +2 -1
  197. package/scripts/lib/config/vault-integration.mjs +7 -1
  198. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  199. package/scripts/lib/config/vault-staleness.mjs +2 -1
  200. package/scripts/lib/config/vault-sync.mjs +2 -1
  201. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  202. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  203. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  204. package/scripts/lib/config.mjs +31 -3
  205. package/scripts/lib/convergence-monitor.mjs +82 -16
  206. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  207. package/scripts/lib/dispatcher/rank.mjs +124 -48
  208. package/scripts/lib/ecosystem-health.mjs +16 -2
  209. package/scripts/lib/eval/engine.mjs +9 -1
  210. package/scripts/lib/eval/session-resolve.mjs +23 -4
  211. package/scripts/lib/events-schema.mjs +48 -0
  212. package/scripts/lib/events.mjs +256 -7
  213. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  214. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  215. package/scripts/lib/frontmatter-guard.mjs +131 -13
  216. package/scripts/lib/gates/gate-full.mjs +26 -0
  217. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  218. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  219. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  220. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  221. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  222. package/scripts/lib/host-identity.mjs +50 -11
  223. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  224. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  225. package/scripts/lib/learnings/io.mjs +60 -6
  226. package/scripts/lib/memory-banner.mjs +20 -8
  227. package/scripts/lib/memory-proposals/store.mjs +30 -22
  228. package/scripts/lib/owner-config-banner.mjs +43 -6
  229. package/scripts/lib/owner-config-loader.mjs +21 -10
  230. package/scripts/lib/owner-interview.mjs +3 -3
  231. package/scripts/lib/owner-yaml.mjs +207 -14
  232. package/scripts/lib/peer-discovery.mjs +20 -2
  233. package/scripts/lib/platform.mjs +108 -15
  234. package/scripts/lib/plugin-update-banner.mjs +406 -0
  235. package/scripts/lib/project-hygiene.mjs +38 -2
  236. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  237. package/scripts/lib/quality-gate.mjs +133 -44
  238. package/scripts/lib/reconcile/emitter.mjs +68 -6
  239. package/scripts/lib/reconcile/engine.mjs +249 -9
  240. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  241. package/scripts/lib/reconcile/writer.mjs +40 -18
  242. package/scripts/lib/scope-gate.mjs +36 -0
  243. package/scripts/lib/session-close-backfill.mjs +125 -18
  244. package/scripts/lib/session-discovery.mjs +57 -3
  245. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  246. package/scripts/lib/session-id.mjs +12 -23
  247. package/scripts/lib/session-identity/own-session.mjs +187 -11
  248. package/scripts/lib/session-lock-shape.mjs +43 -0
  249. package/scripts/lib/session-lock.mjs +5 -10
  250. package/scripts/lib/session-registry.mjs +25 -9
  251. package/scripts/lib/session-schema/constants.mjs +36 -2
  252. package/scripts/lib/session-schema/validator.mjs +38 -4
  253. package/scripts/lib/session-start-probes.mjs +18 -1
  254. package/scripts/lib/session-transition.mjs +1 -1
  255. package/scripts/lib/sessions-canonical.mjs +446 -0
  256. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  257. package/scripts/lib/skill-health/join.mjs +17 -4
  258. package/scripts/lib/state-md.mjs +78 -0
  259. package/scripts/lib/sunset/walker.mjs +6 -0
  260. package/scripts/lib/telemetry/schema.mjs +255 -17
  261. package/scripts/lib/telemetry/sync.mjs +417 -24
  262. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  263. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  264. package/scripts/lib/validate/check-agents.mjs +3 -3
  265. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  266. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  267. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  268. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  269. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  270. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  271. package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
  272. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  273. package/scripts/lib/validate/check-unwired-features.mjs +0 -9
  274. package/scripts/lib/validate/check-validator-registration.mjs +254 -0
  275. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  276. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  277. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  278. package/scripts/lib/vault-backfill/template.mjs +63 -6
  279. package/scripts/lib/vault-mirror/process.mjs +165 -42
  280. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  281. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  282. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  283. package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
  284. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  285. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  286. package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
  287. package/scripts/lib/wave-resource-gate.mjs +133 -7
  288. package/scripts/lib/wave-sizing.mjs +4 -1
  289. package/scripts/lib/wave-transcript-tail.mjs +142 -8
  290. package/scripts/materialize-wave-scope.mjs +32 -9
  291. package/scripts/memory-propose.mjs +146 -8
  292. package/scripts/migrate-cold-start-seed.mjs +4 -1
  293. package/scripts/parse-config.mjs +60 -3
  294. package/scripts/promote-vault-strict.mjs +4 -15
  295. package/scripts/release.mjs +337 -29
  296. package/scripts/repair-invalid-sessions.mjs +3 -3
  297. package/scripts/run-quality-gate.mjs +128 -11
  298. package/scripts/site-numbers.mjs +36 -4
  299. package/scripts/sweep-expired-learnings.mjs +90 -0
  300. package/scripts/sync-vault-schema.mjs +3 -1
  301. package/scripts/telemetry.mjs +2 -2
  302. package/scripts/validate-plugin.mjs +187 -0
  303. package/scripts/validate-wave-scope.mjs +28 -8
  304. package/scripts/vault-consolidate.mjs +3 -11
  305. package/scripts/vault-integration-watcher.mjs +2 -4
  306. package/scripts/vault-mirror.mjs +111 -26
  307. package/scripts/wave-scope-binding.mjs +215 -0
  308. package/skills/_shared/instruction-file-resolution.md +10 -0
  309. package/skills/_shared/parallel-aware-auq.md +31 -2
  310. package/skills/_shared/parallel-aware-preamble.md +18 -4
  311. package/skills/_shared/platform-tools.md +1 -1
  312. package/skills/_shared/state-ownership.md +1 -1
  313. package/skills/architecture/SKILL.md +7 -5
  314. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  315. package/skills/autopilot/SKILL.md +4 -18
  316. package/skills/claude-md-drift-check/SKILL.md +5 -1
  317. package/skills/claude-md-drift-check/checker.mjs +62 -2
  318. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  319. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  320. package/skills/discovery/probes-arch.md +20 -18
  321. package/skills/dispatcher/SKILL.md +3 -2
  322. package/skills/ecosystem-health/SKILL.md +4 -1
  323. package/skills/ecosystem-health/wizard.md +5 -0
  324. package/skills/evolve/SKILL.md +87 -11
  325. package/skills/frontmatter-guard/SKILL.md +11 -5
  326. package/skills/npm-publish/SKILL.md +1 -1
  327. package/skills/reconcile/SKILL.md +38 -2
  328. package/skills/remote-offload/SKILL.md +89 -0
  329. package/skills/session-end/SKILL.md +18 -905
  330. package/skills/session-end/phase-3-6-tail.md +19 -9
  331. package/skills/session-end/plan-verification.md +221 -155
  332. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  333. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  334. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  335. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  336. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  337. package/skills/session-end/references/session-summary-template.md +62 -0
  338. package/skills/session-plan/SKILL.md +49 -0
  339. package/skills/session-start/SKILL.md +41 -900
  340. package/skills/session-start/phase-8-5-express-path.md +1 -1
  341. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  342. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  343. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  344. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  345. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  346. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  347. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  348. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  349. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  350. package/skills/vault-sync/validator.mjs +21 -27
  351. package/skills/wave-executor/SKILL.md +16 -2
  352. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  353. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  354. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  355. package/skills/wave-executor/wave-loop.md +14 -1271
  356. package/templates/_shared/journey-manifest.md +10 -6
  357. package/.cursor/commands/autopilot-multi.md +0 -14
  358. package/.cursor/commands/contract-version-bump.md +0 -14
  359. package/.cursor/commands/journey-audit.md +0 -14
  360. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  361. package/.cursor/skills/daily/SKILL.md +0 -12
  362. package/.cursor/skills/domain-model/SKILL.md +0 -13
  363. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  364. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  365. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  366. package/commands/autopilot-multi.md +0 -74
  367. package/commands/contract-version-bump.md +0 -28
  368. package/commands/journey-audit.md +0 -43
  369. package/pi/prompts/autopilot-multi.md +0 -12
  370. package/pi/prompts/contract-version-bump.md +0 -12
  371. package/pi/prompts/journey-audit.md +0 -12
  372. package/scripts/autopilot-multi.mjs +0 -885
  373. package/scripts/backfill-learnings-expires.mjs +0 -196
  374. package/scripts/backfill-learnings.mjs +0 -203
  375. package/scripts/fleet-instruction-scan.mjs +0 -141
  376. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  377. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  378. package/scripts/lib/webhook-url.mjs +0 -105
  379. package/scripts/lifecycle-sim-v6.mjs +0 -347
  380. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  381. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  382. package/scripts/upload-social-preview.mjs +0 -316
  383. package/skills/_shared/model-selection.md +0 -64
  384. package/skills/contract-version-bump/SKILL.md +0 -219
  385. package/skills/daily/SKILL.md +0 -222
  386. package/skills/daily/generate.sh +0 -92
  387. package/skills/daily/templates/daily.md.tpl +0 -36
  388. package/skills/journey-audit/SKILL.md +0 -269
  389. package/skills/skill-creator/SKILL.md +0 -168
  390. package/skills/ubiquitous-language/SKILL.md +0 -97
  391. package/skills/vault-sync/package-lock.json +0 -40
  392. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  393. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -3,1130 +3,19 @@
3
3
  > Sub-file of the wave-executor skill. Read by the coordinator during wave dispatch.
4
4
  > For pre-execution setup, session type behavior, and error recovery, see `SKILL.md`.
5
5
  > Project-instruction file resolution: `CLAUDE.md` and `AGENTS.md` (Codex CLI) are transparent aliases — see [skills/_shared/instruction-file-resolution.md](../_shared/instruction-file-resolution.md). Wherever this loop mentions a project's `CLAUDE.md`, the alias rule applies.
6
+ > Since #1157 this file is an INDEX. The loop body moved verbatim into `references/`; the three files below are the whole loop, in execution order. Nothing else was added or reworded.
6
7
 
7
- ## Wave Execution Loop
8
+ ## Read in this order
8
9
 
9
- ### 0. Wave-Executor Self-Report (C4#724)
10
+ Per wave, in this order. The two steps marked **MANDATORY-BEFORE-DISPATCH** must not be skipped: skipping either dispatches the wave unguarded and the failure is SILENT no error, no ledger entry, indistinguishable from a clean run.
10
11
 
11
- Run this ONCE at the start of wave execution (before the first wave — not per-wave). The `PreToolUse` skill-invocation matcher does NOT fire for prose-invoked skills, so `wave-executor` is under-counted in `skill-invocations.jsonl` (verified gap: 0 `wave-executor` rows despite a 25-agent session). Emit one `selected` record here so telemetry reflects reality. Best-effort — a write failure NEVER blocks dispatch.
12
+ | # | File | Governs | Read WHEN |
13
+ |---|---|---|---|
14
+ | 1 | [`references/wave-loop-scope-manifest.md`](references/wave-loop-scope-manifest.md) | The two-stage scope manifest — § 3.1 writes ONE `<state-dir>/filescopes/wave-<N>/<agent-id>.json` per agent (the array of path strings the `FILE-SCOPE` prompt injection, the Learnings-Index and `--assert-subset` read), § 3.2 writes the ONE per-wave aggregate of `{id, files}` records that `--assert-disjoint` / `--union` accept. Also `wave-scope.json`, the `blockedCommands` derivation, and the end-of-session teardown. | **MANDATORY-BEFORE-DISPATCH**, every wave — including the coordinator's own `coordinator.json`, which § 3.2 must fold into the aggregate or nothing reads it. Neither stage substitutes for the other. ONE exception: a wave-plan item marked `coordinator-direct: true` dispatches no agents and skips materialization entirely — see file 2. |
15
+ | 2 | [`references/wave-loop-dispatch.md`](references/wave-loop-dispatch.md) | Steps 0 → 1: wave-executor self-report, scope-baseline freeze (§ 0a), pre-dispatch resource gate (§ 0.5), dispatch + the small-batch rule, Started-Set Verification, every pre-dispatch injection (new-directory, path-cousin, fact-staleness, grounding, untracked-overlap, coordinator snapshot, frontmatter-guard, glob-scoped rules, learnings-index, file-scope), the `coordinator-direct: true` zero-agent wave exception, agent-type resolution incl. the `cursor:` and `ssh:` branches, platform-specific dispatch. | AT dispatch, after the Scope Manifest. Its § Started-Set Verification is the second **MANDATORY-BEFORE-DISPATCH-COMPLETES** step: run it immediately after the batch returns and before any review — an agent counts as started on its `meta.json` sidecar and completed on its task-notification, NEVER on the launch ack. |
16
+ | 3 | [`references/wave-loop-review.md`](references/wave-loop-review.md) | Steps 2 → 3b: restore coordinator CWD (§ 2.0), transcript tail and stagnation patterns, open-question collection (§ 3e), the inter-wave quality gate, the auto-fix protocol, the auto-commit checkpoint, persona-reviewer dispatch (§ 5a), adapt plan and dynamic scaling (§ 3), post-wave STATE.md (§ 3a), agent-status telemetry (§ 3a-bis), the persona-gate hook (§ 3b). | AFTER the wave's agents have completed and BEFORE § 4 below. Its § 3a. Post-Wave: Update STATE.md is mandatory before the next wave's Scope Manifest runs. |
12
17
 
13
- ```js
14
- import { appendSkillInvocation } from '$PLUGIN_ROOT/scripts/lib/skill-invocations-schema.mjs';
15
- import path from 'node:path';
16
- try {
17
- await appendSkillInvocation(
18
- path.join(process.cwd(), '.orchestrator/metrics/skill-invocations.jsonl'),
19
- { timestamp: new Date().toISOString(), event: 'selected', skill: 'session-orchestrator:wave-executor', session_id: '<session_id>', phase: 'wave-execution' },
20
- );
21
- } catch { /* telemetry is best-effort — swallow and continue to dispatch */ }
22
- ```
23
-
24
- For each wave, resolve its assigned role(s) from the session plan's role-to-wave mapping:
25
-
26
- **Empty waves:** If the session plan shows a wave with 0 agents (role had no tasks), skip it entirely:
27
- 1. Log in progress update: `## Wave [N] ([Role]) — Skipped (no tasks)`
28
- 2. Update STATE.md: increment `current-wave`, add to Wave History: `### Wave N — [Role] (skipped, no tasks)`
29
- 3. Proceed to next wave immediately
30
- 4. Do NOT write wave-scope.json for skipped waves
31
-
32
- ### 0a. Scope Baseline Freeze (S2 — #896)
33
-
34
- Run this ONCE, immediately after the Self-Report above and before Wave 1 dispatches — never per-wave, same "before the first wave" anchor as the empty-waves rule above. Freezes the session's scope baseline into STATE.md frontmatter so the drift tripwire in step 7a below has a denominator to compare the rest of the session against.
35
-
36
- ```js
37
- import { writeBaseline } from '$PLUGIN_ROOT/scripts/lib/scope-baseline.mjs';
38
-
39
- const result = await writeBaseline({
40
- repoRoot: process.cwd(),
41
- intent: '<one-line session intent, from the agreed session plan>',
42
- ownerBoundary: '<the plan\'s file-scope boundary, e.g. the union of declared agent file scopes>',
43
- plannedFiles: <the RAW array of declared agent file-scope paths, unfiltered
44
- — the UNION of every wave's per-agent "Files:" specs. Pass the array as-is;
45
- `writeBaseline()` filters it internally via `DRIFT_EXCLUDE_PATTERNS`
46
- (the same `filterExcluded()` helper the S2 drift tripwire's numerator
47
- uses in step 7 below), so both sides of the ratio are produced by ONE
48
- code path (#894 review finding F1 — the coordinator no longer has to
49
- remember to pre-filter in prose). MUST be an array — issue #903 removed
50
- the previously-accepted plain pre-counted-number call shape (it was an
51
- unverified re-entry vector for the same F1 filter-bypass bug); anything
52
- else is rejected up front with `reason: 'invalid-planned-files'`.>,
53
- });
54
- ```
55
-
56
- Best-effort — never blocks Wave 1 from dispatching. `result.written === false` with `reason: 'already-frozen'` is expected and silent (a prior wave-executor pass in this same session already froze the baseline — do not re-freeze, do not log). Log any OTHER `reason` (`invalid-planned-files`, `no-state-md`, `unreadable-state-md`, `lock-timeout`, `lock-fs-error`, `unexpected-error`, `size-ceiling`, `frontmatter-unsafe`) as an informational note in the wave progress update — none of these block dispatch.
57
-
58
- Skip entirely when `persistence: false` in Session Config (no STATE.md exists in that mode).
59
-
60
- ### 0.5. Pre-Dispatch Resource Gate (#193)
61
-
62
- Before dispatching agents, the coordinator runs a resource gate to decide whether the wave should proceed as planned, reduce its agent count, or escalate to coordinator-direct. Gated on `$CONFIG["resource-awareness"]` (default: true).
63
-
64
- ```js
65
- import {evaluateWaveResourceGate, formatGateReport} from "scripts/lib/wave-resource-gate.mjs";
66
-
67
- const gate = await evaluateWaveResourceGate({
68
- config: $CONFIG,
69
- plannedAgents: <wave's planned agent count>,
70
- waveRole: "<Discovery|Impl-Core|Impl-Polish|Quality|Finalization>"
71
- });
72
- ```
73
-
74
- **Act on the decision:**
75
-
76
- | Decision | Coordinator action |
77
- |----------|---------------------|
78
- | `proceed` | Dispatch at `gate.agents` (= `plannedAgents`). Include `gate.reasons` in the wave progress update (informational). |
79
- | `reduce` | Dispatch at `gate.agents` (< `plannedAgents`). Log the reduction as a deviation in STATE.md. Include `gate.reasons` in the wave progress update. |
80
- | `coordinator-direct` | Do NOT dispatch subagents. Coordinator executes the wave's tasks directly. Log as a deviation in STATE.md. Continue to `### 1. Dispatch Agents` only for stagnation-pattern detection wording — the section's execution is skipped. |
81
-
82
- Reasons MUST appear in the wave's progress update under a "Resource gate:" bullet. Measurements (RAM free GB, CPU %, concurrent sessions) appear verbatim so the user can trust the decision.
83
-
84
- Probe failures never block a wave — the gate returns `proceed` with a "probe failed (ignored)" reason and the wave continues at the planned count. A config without `resource-thresholds` (legacy pre-#166) returns `proceed` with `"resource-thresholds missing from config — gate skipped"` — a defensive fallback so the gate never crashes the dispatch loop.
85
-
86
- **STATE.md deviation contract (#193):** when the gate returns `reduce` or `coordinator-direct`, append a single timestamped entry to `## Deviations` in `<state-dir>/STATE.md`. Use this exact format so future sessions and the evolve skill can mine for hardware-pattern learnings:
87
-
88
- ```
89
- - [<ISO 8601 UTC>] Wave N resource-gate <reduce|coordinator-direct>: <gate.reasons[0]>. Measurements: ramFreeGb=<N>, cpuLoadPct=<N>, concurrentSessions=<N>. Planned agents=<M>, dispatched=<gate.agents>.
90
- ```
91
-
92
- Skip the deviation entry on `proceed`, even when `concurrentSessions` warns — informational reasons belong in the wave progress update, not in deviations.
93
-
94
- ---
95
-
96
- ### 1. Dispatch Agents
97
-
98
- When `worker-pool.enabled: true` in Session Config, dispatch via `runWavePool()` from `scripts/lib/wave-executor/pool.mjs` with `maxParallel = worker-pool.max-parallel || agents-per-wave` — the bounded cursor is the opt-in alternative that supersedes manual batching. Else fall back to the small-batch Agent() dispatch described below (3–4 calls per message, cumulative up to the wave's `agents-per-wave` cap).
99
-
100
- **Worker-pool and background dispatch compose — neither replaces the other.** The pool is the opt-in for bounded-concurrency *pull* (how many agents may be in flight at once); background dispatch is the default *transport* (whether the coordinator's turn returns before an agent finishes). Pool workers may themselves background-dispatch. Adopting either removes no mechanism from the other, and neither adds a second mechanism to maintain.
101
-
102
- **Worker-pool timing note:** when `worker-pool.enabled: true`, per-agent start and end times are recorded individually in subagents.jsonl as workers pull from the cursor at different moments. Wave-level timings (for progress updates and metrics) are computed as first-worker-start to last-worker-finish, not as a uniform fan-out timestamp.
103
-
104
- Use the **Agent tool** to dispatch this wave's agents. **`run_in_background: true` is ALLOWED and RECOMMENDED for wave dispatch** — measured 2026-08-22 (v2.1.239): under blocking dispatch the coordinator was 143 s incapable of acting between an agent's mid-run escalation and its own next turn — escalation latency equals the batch's remaining runtime. Background dispatch returns turns to the coordinator between agent completions; a running agent received a queued message mid-run and answered ~9 min before its final report.
105
-
106
- Dispatch in **SMALL BATCHES of 3–4 Agent() calls per message** (cumulative up to the wave's `agents-per-wave` cap). Large single-message fan-outs (>4 Agent() calls in one message) remain **FORBIDDEN** — fleet evidence (conf 1.0, 5 sessions) shows they drop Agent() calls SILENTLY (the coordinator receives fewer results than it dispatched, with no error), whereas serial / small-batch dispatch held 13/13 and 8/8. That evidence PREDATES backgrounding and is untouched by it; what backgrounding changes is the *cost* of batching, which is now near zero — a background batch returns its launch acks immediately, so batches no longer serialize the wave. Dispatch a batch, let its acks return, then dispatch the next, until every planned agent is started; then run **Started-Set Verification** below. See `docs/specs/2026-07-02-fleet-mining-followup-grill.md` (C4) for the batching-policy rationale. The `worker-pool.enabled: true` path (above) is the mechanised opt-in alternative to manual batching.
107
-
108
- Read each wave's dispatch metadata from the session plan header (e.g., `(4 agents, parallel, isolation: worktree)`). When the plan specifies `isolation`, use it verbatim. When the plan does not specify, resolve the effective value via `resolveIsolation({ agentCount, sessionType, collisionRisk, configIsolation })` from `scripts/lib/wave-sizing.mjs` — the graduated default (#194) replaces the previous session-type-only switch. Pass the resolved value to each Agent() tool call per `circuit-breaker.md` (omit the parameter when resolved to `none`).
109
-
110
- After resolving `isolation`, compute the wave's enforcement via `resolveEnforcement({ isolation, configEnforcement })` (same module) and write it into `wave-scope.json` under `enforcement`. When isolation resolves to `none`, enforcement auto-promotes from `warn` → `strict` unless the user explicitly set `off` — this ensures the scope hook is hard, not informational, when worktree-level isolation is absent.
111
-
112
- Before dispatching, verify the wave's agent count does not exceed `$CONFIG.agents-per-wave` — if it does, warn the user and request plan revision.
113
-
114
- #### Contract-Lock Serialization (Pattern A, #730/H1)
115
-
116
- When the session plan marks a wave task `contract-lock: true` (session-plan Step 3.5 step 6), dispatch that single agent ALONE as the first batch and WAIT for its **task-notification** (`<status>completed</status>`) before dispatching the disjoint fan-out batches — NOT for its tool-result, which under background dispatch is the immediate launch ack and would release the fan-out against an unfrozen contract (§ Started-Set Verification). The lock agent freezes the shared contract (interfaces/schemas/shared types/constants) so the N follow-on agents build against a fixed surface instead of racing to invent it. Never place the contract-lock agent in the same batch as the impl agents — its output is an input to theirs. The contract file MUST NOT appear in any follow-on agent's allowedPaths (read-only reference). If the lock agent reports STATUS: partial/failed, PAUSE the fan-out and surface the choice via AskUserQuestion (proceed with partial contract / re-dispatch lock / abort wave).
117
-
118
- #### Started-Set Verification (fail-loud — #724, #1115)
119
-
120
- Once all batches for the wave have been dispatched, verify the **started set** against the planned agent list (the agents named in the session plan for this wave). This closes the silent-drop failure class that motivated the small-batch default above (a large fan-out drops calls with no error).
121
-
122
- **Never count launch acks.** Under background dispatch every Agent() call returns an immediate `Async agent launched successfully` ack. Counting acks against the plan would report full success with zero work done — a fail-silent hole inside the fail-loud check. Two distinct signals, each with its own source:
123
-
124
- - **STARTED** — the agent's `agent-<id>.meta.json` sidecar exists under `~/.claude/projects/<encoded-repo-path>/<session-uuid>/subagents/`. The harness writes it at spawn, carrying `toolUseId` + `description` (measured 2026-08-25). Same substrate the tailer reads — see the header of `scripts/lib/wave-transcript-tail.mjs`. The `subagents/` directory does not exist until the first spawn of the session.
125
- - **COMPLETED** — the agent's task-notification carrying its `<tool-use-id>` and `<status>completed</status>`. NEVER the launch ack. `hooks/pre-task-scope-disjoint.mjs` (`ASYNC_LAUNCH_ACK` constant + `buildTranscriptIndex()`) already implements exactly this ack-vs-completion distinction across both the sync and async record shapes — **cite it as the reference implementation; do not restate its logic here and do not fork a second one.**
126
-
127
- Three distinguishable states, each with its own action:
128
-
129
- | State | Signal | Action |
130
- |---|---|---|
131
- | **never-started** | no `meta.json` sidecar after the batch's acks returned | Silent drop. **Re-dispatch ONLY the missing agents in a fresh batch** (3–4 per message) before proceeding to Review. Do NOT re-dispatch agents that already started — that would duplicate their file writes. **Before dispatching any re-dispatch (or fix-pass) batch, re-run the Pre-Dispatch Scope-Union Assertion (§ Scope Manifest #3, #796) for each re-dispatched agent** — `allowedPaths` MUST NOT shrink while sibling agents of this wave are still running, or the re-dispatched agent's legitimate writes will be denied by Gate 7. |
132
- | **started-but-never-returned** | sidecar present, no task-notification | The tailer's territory (step 2.0-bis). Do NOT re-dispatch blindly — the agent may be inside one long tool call (transcripts flush per turn, so it is invisible meanwhile), and a second copy would race it on the same file scope. Inspect its `agent-<id>.jsonl` transcript, then decide. |
133
- | **completed** | task-notification with `<status>completed</status>` | Proceed to `### 2. Review Agent Outputs`. |
134
-
135
- - Record `agent_count_planned` (from the plan), `agent_count_started` (distinct agents with a `meta.json` sidecar, after any re-dispatch) and `agent_count_completed` (distinct agents whose task-notification arrived) in the wave metrics (see § Capture wave metrics). A persistent `planned > started` gap after re-dispatch is a silent drop; a `started > completed` gap at wave end is an agent that never returned. Both are deviations — log them to STATE.md `## Deviations`.
136
-
137
- #### Pre-Dispatch New-Directory Detection (#243)
138
-
139
- > **Motivation:** Claude Code's worktree merge-back fails silently when an agent creates a new directory inside the worktree — the new directory is not copied back to the coordinator's working tree (learning `agent-tool-worktree-no-sync-regression`, conf 0.90, 3rd-consecutive observation). The fix is to detect this condition BEFORE resolving isolation and force `isolation: 'none'` so worktree is never used for those agents, eliminating the regression rather than trying to recover from it (learning `wave3-isolation-none-dispatch`, conf 0.75, proven-pattern).
140
-
141
- Run this step only when `configIsolation` (read from the Execution Config or `$CONFIG.isolation`) is `'auto'`. If the user explicitly set `configIsolation: 'none'`, skip entirely — user override already achieves the desired outcome. If the user explicitly set `configIsolation: 'worktree'`, honour it but emit an ⚠ warning (see branch 4 below).
142
-
143
- ```js
144
- import fs from 'fs';
145
- import path from 'path';
146
-
147
- // configIsolation: resolved from Execution Config or $CONFIG.isolation (default 'auto')
148
- // agentSpecs: array of agent specifications from the session plan for this wave
149
- // Each spec has: { subagent_type, fileScope: string[] } (fileScope = "Files:" entries)
150
-
151
- function detectNewDirAgents(agentSpecs, repoRoot) {
152
- // Returns the count of agents whose scope includes at least one new (non-existent) directory.
153
- let newDirCount = 0;
154
- for (const agent of agentSpecs) {
155
- const willCreateNewDir = (agent.fileScope ?? []).some((scopePath) => {
156
- // Resolve relative to repo root; handle globs by taking the literal dirname.
157
- const resolved = path.resolve(repoRoot, scopePath);
158
- const dir = path.dirname(resolved);
159
- return !fs.existsSync(dir);
160
- });
161
- if (willCreateNewDir) newDirCount++;
162
- }
163
- return newDirCount;
164
- }
165
-
166
- const repoRoot = process.cwd(); // coordinator CWD restored by Step 2.0 before this wave
167
- const newDirAgentCount = detectNewDirAgents(agentSpecs, repoRoot);
168
-
169
- // Branch 1 — no new directories detected, configIsolation: 'auto' → normal resolution path
170
- if (newDirAgentCount === 0 && configIsolation === 'auto') {
171
- // Proceed to resolveIsolation() unchanged.
172
- }
173
-
174
- // Branch 2 — new directories detected, configIsolation: 'auto' → force isolation to 'none'
175
- if (newDirAgentCount > 0 && configIsolation === 'auto') {
176
- configIsolation = 'none'; // override BEFORE calling resolveIsolation()
177
- console.warn(
178
- `⚠ Pre-dispatch: ${newDirAgentCount} agent(s) in this wave will create new directories ` +
179
- `— isolation forced to 'none' per learning agent-tool-worktree-no-sync-regression (conf 0.90). ` +
180
- `Reason: Claude Code worktree merge-back fails on new directories (issue #243).`
181
- );
182
- // NOTE: resolveEnforcement() will auto-promote 'warn' → 'strict' because isolation resolves
183
- // to 'none'. The scope hook therefore becomes a hard barrier (not informational) for this wave —
184
- // document this in the wave progress update so the operator understands enforcement escalated.
185
- }
186
-
187
- // Branch 3 — configIsolation: 'none' set explicitly by user → skip detection entirely
188
- if (configIsolation === 'none') {
189
- // User override respected. No change needed.
190
- }
191
-
192
- // Branch 4 — configIsolation: 'worktree' set explicitly by user → honour but warn if new dirs exist
193
- if (configIsolation === 'worktree' && newDirAgentCount > 0) {
194
- console.warn(
195
- `⚠ Pre-dispatch: ${newDirAgentCount} agent(s) will create new directories AND ` +
196
- `isolation is explicitly set to 'worktree'. ` +
197
- `Known regression: Claude Code merge-back silently drops new directories (issue #243). ` +
198
- `Override configIsolation to 'none' to avoid data loss.`
199
- );
200
- // Proceed with worktree as requested — user accepted the risk.
201
- }
202
-
203
- // Branch 5 — configIsolation: 'auto', newDirAgentCount === 0 → no-op (same as Branch 1)
204
- // Explicit for clarity; covered by Branch 1 above.
205
- ```
206
-
207
- After running this detection block, call `resolveIsolation({ agentCount, sessionType, collisionRisk, configIsolation })` with the (possibly overridden) `configIsolation`. Then call `resolveEnforcement({ isolation, configEnforcement })` as normal — when isolation resolved to `'none'` via Branch 2, enforcement auto-promotes `warn` → `strict`, which MUST be noted explicitly in the wave progress update.
208
-
209
- #### Pre-Dispatch: Path-Cousin-Guard Injection (#730.3)
210
-
211
- Before dispatching each agent whose fileScope contains a NEW (non-existent) file target, check for existing "cousin" files with a similar basename elsewhere in the repo — prevents the framing-wrong class where an agent creates `scripts/lib/foo/bar.mjs` while `scripts/lib/bar.mjs` already exists and serves the same purpose.
212
-
213
- **Detection (mechanical, reuses the new-file scan from #243 above):** for each not-yet-existing file target `<newPath>` in an agent's fileScope, take `basename(<newPath>)` minus extension; skip generic basenames (`index`, `utils`, `main`, `config`, or length ≤ 3 chars — false-positive control). Then:
214
-
215
- git ls-files | grep -iE "(^|/)<basename>\.[a-z]+$"
216
-
217
- **If ≥1 candidate found**, prepend to the agent's prompt:
218
-
219
- <PATH-COUSIN-GUARD>
220
- Before creating <newPath>, verify it does not duplicate existing functionality — candidate file(s) with a similar name exist: <candidates>. Read each candidate first. If one already serves this purpose, extend/reuse it instead. Only proceed with the new file if you can state why the existing candidate(s) don't fit.
221
- </PATH-COUSIN-GUARD>
222
-
223
- **If 0 candidates:** dispatch unchanged — same silent-no-op convention as Grounding Injection / Frontmatter-Guard above. Never blocks dispatch.
224
-
225
- #### Pre-Dispatch: Fact-Staleness Annotation (#908)
226
-
227
- Facts an earlier wave measured get quoted into this wave's prompts as briefing truth — and they decay. In the #908 incident the impl agents found 14 commits where the brief said 9, a clean tree where it said 5 dirty, 92 learnings where it said 40, a file that no longer existed, and a closed epic briefed as critical-open; the worst class was line numbers, which drifted three times and forced 225 citations onto symbol+grep form. Annotating a fact costs one prompt line. Re-briefing a wave on wrong numbers costs the wave.
228
-
229
- **What is a fact here:** any repo-state value carried from an earlier wave's report into this prompt — counts (commits, files, tests, issues, learnings), line numbers, file existence, "session X is running", issue/epic open-closed state. NOT design decisions, task assignments, or judgements: those do not decay.
230
-
231
- **Trigger — annotate when ANY of these holds (no judgement call):**
232
-
233
- 1. `now − measured_at ≥ 5 min`
234
- 2. `measured_at` is absent
235
- 3. the peer probe below reported `live: true` for the repo the fact is about
236
-
237
- Threshold derived from `.orchestrator/metrics/subagents.jsonl` (n=340 wave boundaries, agent runtime median 3.5 min): a fact's age at its FIRST cross-wave citation brackets [median 2.5 min, median 9.9 min] depending on where in the producing agent's run it was measured. 5 min sits at the conservative end of that bracket; the cost asymmetry breaks the tie downward. **Corollary: a fact from an earlier wave almost always trips rule 1 — when in doubt, annotate.** `measured_at` comes from the producing agent's report, which `hooks/post-subagent-discovery-validator.mjs` already asks for (PSA-006 point 4).
238
-
239
- **Peer signal — once per wave, plus once per distinct foreign repo cited:**
240
-
241
- ```bash
242
- node "$PLUGIN_ROOT/scripts/lib/peer-discovery.mjs" --check-live "<repoRoot>" --json
243
- # → {"live":false,"reason":"no-lock","probe":"lock-only","peerCount":0,"peer":null}
244
- ```
245
-
246
- Read `live` from the payload; the exit code reports whether the probe RAN (`0` verdict produced, `1` usage error, `2` internal failure), never the verdict itself. Probe selection is automatic and needs no flag: the coordinator's own working copy takes the `full` probe (worktrees + registry + STATE.md), any other repo takes `lock-only` (two sync calls, no git). Own-vs-foreign is decided by repo IDENTITY, not path nesting — a parent directory that is itself a repo (`~/Projects/<workspace>`) is foreign, not "mine".
247
-
248
- Call it for the coordinator's own repo even when every cited fact is about that repo — the own-repo probe self-excludes this session and answers "is another operator session writing into my working copy right now", which is exactly the #908 "14 vs 9 commits" class. `live: true` sets the threshold to **0** for that repo: every state fact about it is asserted, never established, however fresh. The probe is fail-safe (unmeasurable ⇒ `live: true`, including `probe: "full-degraded"` when the peer surfaces returned demonstrably incomplete data), so a probe failure annotates more, never less — and so does a non-zero exit: treat exit `1`/`2` as `live: true`.
249
-
250
- **Annotation format** — in the agent prompt, replace the bare value with:
251
-
252
- ASSERTED (age <N> min, source W<k>/<agent>): <value>. Verify command: <cmd>. Run it before relying on this.
253
-
254
- **When no measurement command can be named** (rule 2, and the case the validator is meant to catch upstream), do not restate the value at all — a number nobody can re-derive is not a fact:
255
-
256
- UNVERIFIED (no measurement command, source W<k>/<agent>): <claim>. Establish it yourself before relying on this.
257
-
258
- **Worked examples:**
259
-
260
- | Fact | Decision |
261
- |---|---|
262
- | "13 broken paths", W1-D2, `measured_at` 10:35, cited at 11:20 | Rule 1 (45 min ≥ 5) → `ASSERTED (age 45 min, source W1/D2): 13 broken paths. Verify command: <the grep D2 ran>. Run it before relying on this.` |
263
- | "the coordinator mis-measured 4 numbers" — no measurement command | Rule 2 → `UNVERIFIED` form; the value is dropped, the claim becomes the agent's own task |
264
- | Any count about a foreign repo whose peer probe reports `live: true` | Rule 3 → annotate regardless of age; age may still be printed but is not the reason |
265
-
266
- **Never blocks dispatch** — same silent-no-op convention as the injectors above. When facts cannot be annotated for any reason, dispatch proceeds; annotating more is always the safe direction.
267
-
268
- #### Agent-Type Resolution
269
-
270
- Each agent in the session plan specifies a `subagent_type`. Use that value directly when dispatching:
271
-
272
- ```
273
- For each agent in this wave:
274
- Agent({
275
- description: "<3-5 word summary>",
276
- prompt: "<COMPLETE task context including:
277
- - What to do (specific, measurable)
278
- - Which files to read/modify (exact paths)
279
- - Acceptance criteria (how to verify done)
280
- - Relevant patterns — injected automatically as the <APPLICABLE-RULES> block (see Pre-Dispatch: Glob-Scoped Rule Injection below)
281
- - Relevant past learnings — injected automatically as the <LEARNINGS-INDEX> block, computed PER AGENT from its file scope (see Pre-Dispatch: Learnings-Index Injection below)
282
- - Any repo-state fact carried from an earlier wave: in the ASSERTED/UNVERIFIED form, never as a bare value (see Pre-Dispatch: Fact-Staleness Annotation above)
283
- - VCS issue reference if applicable
284
- - What NOT to touch (other agents' files)
285
- >",
286
- subagent_type: "<from session plan>", // resolved agent type
287
- run_in_background: true // RECOMMENDED — verify the started set via meta.json sidecars, never via the launch ack (§ Started-Set Verification)
288
- })
289
- - Turn budget and status reporting: "You have a maximum of [maxTurns] turns for this task. If you cannot complete within this budget, report STATUS: partial with what was accomplished and what remains. At the end of your work, report STATUS: done (all acceptance criteria met) or STATUS: partial (some criteria unmet — list which ones)."
290
- - Optional open-questions reporting (Close Handover-Alignment-Gate, PRD 2026-07-07): "If you encountered a genuinely unresolved, user-facing question you could not answer within your task scope, report it as an additional line: OPEN-QUESTIONS: <question> | context: <one-line why this is unresolved> | candidates: <opt A / opt B>. This line is optional — omit it entirely when you have no such question. Do not use it for questions you could resolve yourself by reading more code."
291
- ```
292
-
293
- ##### Third branch: foreign-model dispatch (`cursor:<model>` — #1150)
294
-
295
- The colon heuristic (§ "How to detect project agents") has **three** readings, not two: no colon = project agent, `session-orchestrator:<agent>` = plugin agent, and `cursor:<model>` = **foreign channel**. When the session plan or `agent-mapping` resolves an agent to `cursor:<model>` (e.g. `impl: cursor:composer-2.5`), do NOT call the Agent tool. Dispatch **coordinator-direct** via `dispatchForeign({ model, prompt, repoRoot, role, runId, timeoutSec })` from `scripts/lib/wave-executor/foreign-dispatch.mjs`. Contract, in the order it binds:
296
-
297
- 1. **`never_foreign` gate first.** `isNeverForeignRole(role)` returns `{ok:false, reason:'never-foreign-role'}` before any worktree or spawn. `NEVER_FOREIGN_ROLES` (impl-core, security-review, migration, release, secrets, incident, refactor-crosscut) is hand-copied from the account-switch routing SSOT; `dispatch-cursor.sh` enforces none of it, so **this adapter is the only gate** — never route around it with a shell call.
298
- 2. **Detached worktree**, `<tmpdir>/so-foreign/<runId>` by default, `git worktree add --detach`. Never under `.claude/worktrees/` — eight readdir scanners in this repo read that path. The stream log is KEPT beside it (`<runId>.log.jsonl`), not trap-deleted.
299
- 3. **Verdict measured at the filesystem, never from the model's prose.** `ok` is true only on exit 0 + no timeout + a non-empty changed set (`git diff --name-only` ∪ `ls-files --others`, since a diff alone is blind to new files). **An empty diff is a failure regardless of what the report says.**
300
- 4. **MANDATORY Claude diff-review gate.** Read `result.diff` and judge it SEMANTICALLY before any merge-back — test-green is not the bar (measured counterexample: the foreign #1149 solution was test-green and semantically wrong). The **coordinator** applies (`git apply`) and commits; the foreign model never touches this repo's index or worktree (PSA-007). On review failure, discard the diff. **`result.hookTampering === true` invalidates the run regardless of `ok`**: the child repointed or rewrote the shared `.git` hooks path (a linked worktree isolates the tree, not `core.hooksPath`), and that write is invisible in `result.diff` — discard and do not merge. `null` means the fingerprint could not be read (not measured), never "clean".
301
- 5. **`removeForeignWorktree({ repoRoot, worktreePath })` after the review** — never before, because a failed run must stay inspectable. It **refuses a `worktreePath` outside `<tmpdir>/so-foreign*`** (returns `{ok:false, reason:'unsafe-...'}` and makes no git call), so it cannot `--force`-remove a sibling session's worktree.
302
-
303
- **Trust boundary.** A foreign run bypasses the ENTIRE hook chain — no `PreToolUse:Agent`, no scope enforcement, no `SubagentStop` telemetry (verified against `hooks/hooks.json`, W1/D4). Everything inside the detached worktree is **untrusted** until a Claude coordinator has read the diff and applied it. The replacement ledger record is the `orchestrator.foreign_dispatch.completed` event the adapter emits (payload in `docs/events-schema.md`), which fires on refusals too — a blocked dispatch is a record, not a silence.
304
-
305
- **Composition.** A foreign run is a background Bash task, so it composes with background `Agent()` dispatch in the same wave. It is **NOT** part of § Started-Set Verification: no `meta.json` sidecar exists for it and no task-notification arrives. Its lifecycle is the adapter's return value — do not count it as planned/started/completed, and do not read its absence from the started set as a drop.
306
-
307
- **Model selection** is not decided here: the account-switch routing SSOT (ADR-002 / `tools/routing/routing.yaml`) owns it. Working defaults: `composer-2.5` for foreign impl; `cursor-grok-4.6-high` for review / test-writing / judgment roles, at `timeoutSec ≥ 900`. Dogfood evidence (2026-08-25): `composer-2.5` on issue #1105 — 165 s, 2 files, +93 lines, mandatory review passed, merged only after independent test verification.
308
-
309
- **Timeout.** `maxTurns` does not exist on this channel — see the § Platform-Specific Dispatch timeout note. The wall-clock SIGTERM (`DEFAULT_TIMEOUT_SEC = 900`, a floor) is the only circuit breaker.
310
-
311
- #### Pre-Dispatch Grounding Injection (#85)
312
-
313
- Before dispatching each agent, prepend a line-numbered GROUNDING block to its prompt for any file in the agent's scope that has recent edit-format-friction history. This helps the agent reference edits by line number instead of re-matching exact character spans, reducing Edit-tool retry loops.
314
-
315
- **Gate:** `$CONFIG."grounding-injection-max-files" > 0` AND `$CONFIG.persistence == true`. When either condition is false, skip the entire step.
316
-
317
- **Per-agent scope** (not per-wave): each agent's file scope comes from its specification in the session plan — the same source used for computing the wave's `allowedPaths` union (see `## Scope Manifest` § 3). An agent with narrow scope gets grounding only for files it will touch.
318
-
319
- **Invocation:** for each agent about to be dispatched, call:
320
-
321
- AGENT_FILES="$(printf '%s\n' "${agent_file_scope[@]}")" \
322
- SESSIONS_JSONL=".orchestrator/metrics/sessions.jsonl" \
323
- EVENTS_JSONL=".orchestrator/metrics/events.jsonl" \
324
- MAX_FILES="$(echo "$CONFIG" | jq -r '."grounding-injection-max-files"')" \
325
- SESSION_ID="<session_id>" WAVE="$wave_num" AGENT_TYPE="<subagent_type>" \
326
- PERSISTENCE="$(echo "$CONFIG" | jq -r '.persistence')" \
327
- bash "$PLUGIN_ROOT/scripts/compute-grounding-injection.sh"
328
-
329
- Capture stdout as `$GROUNDING_BLOCK`. If empty, dispatch the agent unchanged (legacy behavior).
330
-
331
- **Prompt assembly:** when `$GROUNDING_BLOCK` is non-empty, prepend to the agent prompt:
332
-
333
- <GROUNDING_BLOCK>
334
-
335
- Use line numbers above to describe edits precisely instead of re-matching character spans. If a line has changed since this snapshot, re-read the file before editing.
336
-
337
- ---
338
-
339
- <original prompt>
340
-
341
- The helper emits one `orchestrator.grounding.injected` event per injected file to `.orchestrator/metrics/events.jsonl` (routed through `scripts/emit-event.mjs` → the canonical `emitEvent()` path). The helper never returns non-zero; any failure (missing jq, missing events.jsonl, unreadable file) results in silent no-op so wave dispatch is never blocked.
342
-
343
- **Fallback for agents without explicit file scope:** if the session plan's agent specification does not list a "Files:" scope for an agent, fall back to the wave-level `allowedPaths` (from `wave-scope.json`). If that is also empty, skip injection for that agent.
344
-
345
- **Relationship to `### 3c. File-level grounding`:** this pre-dispatch feature is DIFFERENT from the post-wave file-level grounding check. Pre-dispatch grounding injects file content into agent prompts (prevents friction). Post-wave grounding verifies agents stayed within their planned scope (detects scope creep). The two features share no code and run at different times.
346
-
347
- #### Pre-Dispatch Untracked-Overlap Check (#180)
348
-
349
- Claude Code's Agent tool with `isolation: "worktree"` syncs the agent's worktree back into the coordinator's working tree on completion. If the coordinator holds untracked files inside the agent's scope, the sync silently overwrites them — observed as data loss in the 2026-04-19 deep-drift-check session (4 files, ~700 LoC wiped). See issue #180.
350
-
351
- **Apply this check only when dispatching with `isolation: "worktree"`.** For `isolation: "none"` or coordinator-direct execution, skip — there is no merge-back to worry about.
352
-
353
- For each worktree-isolated agent about to be dispatched:
354
-
355
- ```js
356
- import { checkUntrackedOverlap } from '$PLUGIN_ROOT/scripts/lib/pre-dispatch-check.mjs';
357
-
358
- const result = checkUntrackedOverlap({
359
- scope: agentFileScope, // same array used for `allowedPaths`
360
- cwd: process.cwd(),
361
- mode: 'warn', // 'warn' (default) | 'block' | 'off'
362
- });
363
-
364
- if (result.decision === 'block') {
365
- // Refuse dispatch. Report result.message to the user.
366
- // Ask: commit the files, stash them, or rerun with mode=warn to acknowledge.
367
- } else if (result.decision === 'warn') {
368
- // Print result.message to the wave progress update.
369
- // Dispatch proceeds, but the coordinator has an audit trail if data loss occurs.
370
- }
371
- ```
372
-
373
- The helper is stdlib-only and cross-platform. `mode=block` is recommended when the coordinator holds uncommitted work of non-trivial size in the agent's scope — it trades a friction prompt for the guarantee that the merge-back cannot silently overwrite. `mode=warn` keeps the historical behavior and simply records the risk. `mode=off` short-circuits entirely.
374
-
375
- This is a downstream backstop: the underlying worktree merge-back strategy lives in the Claude Code harness and is outside this plugin's control. The correct fix (preserve untracked coordinator files during merge-back) must come upstream. Until then, this check is the only defense.
376
-
377
- #### Pre-Dispatch Coordinator Snapshot (#196)
378
-
379
- Before dispatching agents for this wave, checkpoint any uncommitted coordinator work as a git stash snapshot. This is a backup — it does NOT touch the working tree and does NOT block dispatch on failure.
380
-
381
- **Gate:** `$CONFIG.persistence == true`. When `persistence: false`, skip this step entirely.
382
-
383
- ```js
384
- import { saveSnapshot } from '$PLUGIN_ROOT/scripts/lib/coordinator-snapshot.mjs';
385
-
386
- const snap = await saveSnapshot({
387
- sessionId: '<session_id>',
388
- waveN: <wave_num>,
389
- label: 'pre-dispatch',
390
- });
391
-
392
- if (!snap.ok) {
393
- // Non-fatal — log the error in the wave progress update but do not block.
394
- console.warn(`coordinator-snapshot: snapshot failed (non-fatal): ${snap.error}`);
395
- }
396
- // snap.skipped === true when the working tree is clean; also fine, dispatch continues.
397
- ```
398
-
399
- The snapshot is stored under `refs/so-snapshots/<sessionId>/wave-<N>-pre-dispatch`. It survives Claude process termination (unlike memory-only state) and is cleaned up by session-end on clean close (see session-end/SKILL.md). Orphaned snapshots from crashed sessions are reclaimed by `gcSnapshots({olderThanDays: 14})`.
400
-
401
- See issue #196 for the full rationale. This is complementary to the untracked-overlap check above (#180 is scope-level detection; this is working-tree-level backup).
402
-
403
- #### Pre-Dispatch: Frontmatter-Guard Injection (#328)
404
-
405
- Before constructing each agent's prompt, decide if the schema snippet must be injected:
406
-
407
- 1. Compute task vault-scope: `import { detectVaultTaskScope } from 'scripts/lib/frontmatter-guard.mjs'`. Pass the agent's task description + file scope (paths the agent is allowed to write).
408
- 2. **If vault-scoped (returns `true`):**
409
- a. Call `readVaultSchema()` from the same module.
410
- b. If the schema read returned non-null, call `generateFrontmatterSnippet(schema)` to get a Markdown block.
411
- c. Prepend the block to the agent's prompt under a clear separator:
412
-
413
- ```
414
- <FRONTMATTER-GUARD>
415
- <generated snippet>
416
- </FRONTMATTER-GUARD>
417
-
418
- <original prompt>
419
- ```
420
- d. If `readVaultSchema()` returned `null` (schema source absent), emit stderr WARN `Frontmatter-guard: schema source missing at <path> — agent prompts will not include schema enums`. Continue dispatch without injection (do NOT block).
421
- 3. **If not vault-scoped:** dispatch as today, no injection.
422
-
423
- Performance note: `readVaultSchema()` caches by file mtime, so repeated calls within a wave are free. The schema read happens at most once per wave-executor run.
424
-
425
- Behaviour change: agents writing vault notes now receive the canonical schema enums + per-type examples directly in their prompt context. This eliminates the agent-guessing failure class documented in #328.
426
-
427
- #### Pre-Dispatch: Glob-Scoped Rule Injection (#336/#694)
428
-
429
- After `wave-scope.json` is written for this wave and before assembling the `Agent()` prompt, inject the wave's applicable rule set into each dispatched agent's prompt. This wires the `loadApplicableRules()` loader (`scripts/lib/rule-loader.mjs`) — dormant since #336 — into the live per-wave prompt assembly via the thin CLI `scripts/print-applicable-rules.mjs`.
430
-
431
- > **⚠ Measure before you inject — on Claude Code this step is usually a NET LOSS (#931b).** `docs/instruction-delivery.md` measured the delivery path on 2026-07-30: every `.claude/rules/*.md` already reaches a dispatched agent through Claude Code's **native project-instruction loading**, so a `$RULES_BLOCK` prepended on top arrives a *second* time. Measured on a real wave: the scoped block was 122,875 B against a 169,961 B corpus — glob scoping saved **4.0%**, of which 85.5% came from the tier axis alone, while injecting alongside undiminished native delivery cost **+72%** (292,836 B). The coordinator SHOULD therefore check the block's size before prepending it, and MAY skip the injection with a logged Deviation when the harness already delivers the corpus natively — that is not a shortcut, it is the measured decision. Inject unconditionally only on a harness that does NOT auto-load `.claude/rules/` (Codex CLI, Pi, Cursor), where this block is the sole delivery path and the saving is real. See `docs/instruction-delivery.md` §1.2 and §5.
432
-
433
- **Gate:** runs when `.claude/rules/` exists. When it does not, the CLI prints nothing and exits 0 — zero behaviour change. This step never blocks dispatch: any non-zero exit or empty output means "inject nothing, continue" (same best-effort framing as Pre-Dispatch Grounding Injection above).
434
-
435
- **Per-wave scoping (not per-agent):** the rule set is computed ONCE per wave from the wave's `allowedPaths` union (the same `wave-scope.json` source used elsewhere), not per agent. The CLI resolves `scopePaths` from `allowedPaths`, `mode` from the `session-type:` frontmatter in `.claude/STATE.md`, and `hostClass` from `.orchestrator/host.json` — all overridable, all degrading to "no gating" when unreadable.
436
-
437
- **Invocation:** once per wave, run from the repo root and capture stdout as `$RULES_BLOCK`:
438
-
439
- RULES_BLOCK="$(node "$PLUGIN_ROOT/scripts/print-applicable-rules.mjs" --context wave 2>/dev/null)"
440
-
441
- `--context wave` (issue #692) excludes `tier: coordinator-only` rules (owner-persona, lsp, mvp-scope, loop-and-monitor) from the wave-agent prompt — those are operator/coordinator-context rules a wave implementation agent does not need. `tier: always` and `tier: wave-only` rules are unaffected; omitting the flag (or passing `--context coordinator`) disables wave-tier exclusion. Use `--wave-scope <path>` only if `wave-scope.json` is not at the default `.claude/wave-scope.json`. The CLI returns:
442
- - a Markdown block (header `## Applicable Rules (scoped to this wave)`, a preamble naming the block's fence token, then each matching rule's raw content wrapped in `<rule-<token> index="i/N" src="<repo-relative path>">` … `</rule-<token>>`) when one or more rules apply, OR
443
- - empty output (exit 0) when no rules match — in which case prepend nothing.
444
-
445
- **Prompt assembly:** when `$RULES_BLOCK` is non-empty, prepend it to EACH agent's prompt in this wave under a clear separator:
446
-
447
- <APPLICABLE-RULES>
448
- $RULES_BLOCK
449
- </APPLICABLE-RULES>
450
-
451
- <original prompt>
452
-
453
- When `$RULES_BLOCK` is empty (no `.claude/rules/`, no matching rules, or any CLI failure), dispatch the agent unchanged. Because the block is computed once per wave, the same `$RULES_BLOCK` is reused for every agent dispatched in this wave — narrow waves (e.g. only `scripts/**` or only `tests/**` files) receive a smaller rule set, which is the #336 token-reduction payoff.
454
-
455
- This replaces the older prose slot "Relevant patterns from `<state-dir>/rules/`" in the `Agent()` template above: the `<APPLICABLE-RULES>` block IS that injection, now mechanically scoped to the wave instead of left to the coordinator's judgement.
456
-
457
- #### Pre-Dispatch: Learnings-Index Injection (#1014)
458
-
459
- > **Read this first — it is computed PER AGENT, unlike the block directly above.** The rule injection you just read states "Per-wave scoping (not per-agent): the rule set is computed ONCE per wave". This step is the opposite: **run the CLI once for EACH agent**, because per-agent differentiation IS the acceptance criterion — an agent scoped to `scripts/lib/learnings/**` must receive different entries than its sibling scoped to `skills/**`. Model it on **Pre-Dispatch Grounding Injection (#85)** above, not on its immediate neighbour. Computing it once and reusing it across the wave silently reduces this feature to a worse version of the coordinator banner that already exists.
460
-
461
- 89 learnings have accumulated across 233 sessions, and a dispatched wave agent receives **zero** of them: the only read paths are a coordinator banner, an autopilot call, and a nudge banner — none reaches an agent prompt. This step closes that loop by prepending a compact, relevance-ranked INDEX of learnings to each agent's prompt.
462
-
463
- **Why this does not repeat the #931b mistake.** `docs/instruction-delivery.md` measured that adding a SECOND delivery path alongside Claude Code's native project-instruction loading costs **+72%** (292,836 B vs 169,961 B) — which is why the rule block above carries a "measure before you inject" warning. That warning does **not** transfer here, and not as a matter of argument: learnings have no native delivery path to duplicate. `learnings.jsonl` lives under `.orchestrator/metrics/`, is not a project-instruction file, is not `@`-imported from CLAUDE.md, and reaches nothing agent-facing today. This is the FIRST path, and it rides the dispatch-prompt channel this repo already owns and writes itself — no new mechanism is introduced. It is also bounded by a code constant (`LEARNINGS_INDEX_MAX_CHARS = 2000`, ~1.1% of the measured 178,095 B per-agent prompt baseline) with no `0 = unlimited` sentinel, so it cannot grow into the corpus it indexes.
464
-
465
- **An INDEX, not a corpus.** One line per learning plus a retrieval pointer; an agent that needs a full entry greps it by subject. Measured: 12 entries in this form = 1,469 B.
466
-
467
- **Gate:** runs when `.orchestrator/metrics/learnings.jsonl` exists. When it does not — or when nothing clears the confidence floor, or the corpus is unreadable — the CLI prints nothing and exits 0. Same best-effort convention as every injector above (Grounding `:307`, Frontmatter-Guard `:386`, Path-Cousin-Guard `:208`): silent no-op on any failure, **never blocks dispatch**. Any non-zero exit means "inject nothing, continue".
468
-
469
- **Zero new coordinator obligations.** The per-agent file scope this needs is the SAME `$AGENT_FILESCOPE_JSON` — `<state-dir>/filescopes/wave-<N>/<agent-id>.json` — that `## Scope Manifest` § 3.1 already requires you to write for every agent, and that the Scope-Union Assertion (#796) then consumes. Reuse that file — do not write a second one, and never a temp copy.
470
-
471
- **Invocation:** once per agent, immediately after that agent's `$AGENT_FILESCOPE_JSON` is written, capture stdout as `$LEARNINGS_INDEX`:
472
-
473
- LEARNINGS_INDEX="$(node "$PLUGIN_ROOT/scripts/print-learnings-index.mjs" \
474
- --file-scope "$AGENT_FILESCOPE_JSON" \
475
- --task-text "<the agent's task title / one-line description>" 2>/dev/null)"
476
-
477
- `--task-text` is optional and feeds the token axis of the affinity primitive; omitting it yields path-only ranking. **Resolution ladder** (mirrors Grounding Injection `:309`): the agent's own `--file-scope` → the wave-level `allowedPaths` from `.claude/wave-scope.json` (automatic fallback when the agent has no declared "Files:" scope) → empty scope, in which case only the general tier is selected. Caps are `--max-scoped` (default 8) and `--max-global` (default 4) — **split, never shared**, so the general tier can never crowd out the per-agent signal.
478
-
479
- **Prompt assembly:** when `$LEARNINGS_INDEX` is non-empty, prepend it to THAT agent's prompt:
480
-
481
- <LEARNINGS-INDEX>
482
- $LEARNINGS_INDEX
483
- </LEARNINGS-INDEX>
484
-
485
- <original prompt>
486
-
487
- When it is empty (no corpus, no qualifying entries, or any CLI failure), dispatch that agent unchanged — the prompt is then byte-identical to the legacy one.
488
-
489
- **Instrumentation (why this one is measurable and its neighbours are not).** The rule injection above is a SHOULD and emits no signal either way, so "did the coordinator actually inject?" has been unanswerable after the fact — a gap the #1014 discovery wave had to leave open. This CLI emits `orchestrator.learnings.index.injected` to `.orchestrator/metrics/events.jsonl` (via `scripts/emit-event.mjs`, the canonical `emitEvent()` path — the same route `scripts/compute-grounding-injection.sh` uses for `orchestrator.grounding.injected`), carrying `count`, `scope_matched`, `global_count`, `candidates`, `truncated`, `bytes`, and `scope_source`. The before/after measurement is therefore a fact in the event log, not a matter of prose compliance. Emission is best-effort and suppressible with `--no-event`; a failed emit never blocks dispatch.
490
-
491
- #### Pre-Dispatch: File-Scope Injection (#1020)
492
-
493
- > **Read this first — this block is PER AGENT, unlike `#### Pre-Dispatch: Glob-Scoped Rule Injection (#336/#694)` above, which states "Per-wave scoping (not per-agent): the rule set is computed ONCE per wave".** Model it on **Pre-Dispatch Grounding Injection (#85)** — same cadence, same per-agent source. This injector legitimately has BOTH cadences (per-agent for the brief, per-wave for the § Scope Manifest union), which is exactly what makes the collapse tempting: reuse ONE agent's block for the whole batch and every agent reads the territory of every OTHER agent as its own. Deconfliction would then be **lifted rather than enforced**, and the double assignment § 3.2 exists to catch becomes invisible in the one channel where an agent could still notice it.
494
-
495
- **Invocation:** for each agent, read `<state-dir>/filescopes/wave-<N>/<agent-id>.json` (= `$AGENT_FILESCOPE_JSON`) — the SAME file written in § Scope Manifest 3.1, not a re-derivation from the session plan and not a temp copy — and prepend its entries to that agent's prompt, one path per line:
496
-
497
- FILE-SCOPE — exactly these:
498
- ```
499
- <one path or glob per line, verbatim from that agent's scope file>
500
- ```
501
-
502
- Marker line plus fenced block, in that order: `hooks/pre-task-scope-disjoint.mjs` extracts the scope from the prompt by finding the marker and taking the FIRST fenced block after it, so this shape is what makes an agent's declared territory machine-readable at dispatch time. An unparseable or absent block resolves to ALLOW there, so a malformed injection degrades to today's behaviour rather than blocking dispatch. When the scope file is missing or empty (Discovery waves), inject nothing and dispatch unchanged.
503
-
504
- > **Registration note.** That hook was armed in `hooks/hooks.json` on 2026-08-14, after a green Full Gate. Its `PreToolUse` matcher is **`Agent`** — measured over 12 archived transcripts of this repo, `Agent` accounts for 147 of 147 dispatch `tool_use` blocks. A `Task` matcher would hit the unrelated todo family (`TaskCreate`/`TaskUpdate`/`TaskGet`/…) and never once fire on a dispatch: armed and inert, the failure mode that reads as done. It is deliberately absent from `hooks-codex.json` / `hooks-cursor.json` / `hooks-pi.json` — those platforms have no `Agent` dispatch tool, so the asymmetry is registered in `DOCUMENTED_ASYMMETRIES` rather than papered over with a matcher that can never fire.
505
-
506
- #### Structured Reasoning (STATE:/PLAN:) — opt-in via `reasoning-output: true` (#79)
507
-
508
- When `$CONFIG.reasoning-output` is `true`, append the following block to every agent prompt. The pattern is adapted from the BitGN PAC Agent's Soft-SGR: short structured transparency lines before tool invocations, without forcing structured output. Leave the block OUT when the flag is `false` (default) — this preserves exact legacy prompt behavior.
509
-
510
- ```
511
- ## Reasoning format
512
-
513
- Before every meaningful tool call, emit two single-line markers so the coordinator can trace your thinking:
514
-
515
- STATE: <one-line summary of what you currently know about the task — files read, constraints, blockers>
516
- PLAN: <one-line summary of what you are about to do and why>
517
-
518
- Rules:
519
- - Keep each line under ~160 characters. Do not nest markdown or code blocks inside these lines.
520
- - Emit them together, STATE first then PLAN, immediately before the tool call they describe.
521
- - Skip them for trivial read-back tool calls (e.g., re-reading a file you just wrote). Do not spam them.
522
- - These markers DO NOT replace your normal text output — they supplement it. Continue writing normal progress updates.
523
- ```
524
-
525
- **Resolution chain** (if the plan does not specify `subagent_type` for an agent):
526
-
527
- 1. **Discovery waves** → `"Explore"` (always, read-only)
528
- 2. **Quality review** → `"session-orchestrator:session-reviewer"` (always)
529
- 3. **Impl-Core / Impl-Polish / Quality (test-writing)** → check in order:
530
- a. Project agent matching the task domain (e.g., `"database-architect"` for DB tasks)
531
- b. Plugin agent (e.g., `"session-orchestrator:code-implementer"`)
532
- c. `"general-purpose"` (final fallback)
533
-
534
- > **Docs-role dispatch (A3):** `docs-writer` is the canonical first-class agent for Docs-role tasks (audience-split documentation generation per `skills/docs-orchestrator/SKILL.md`). It flows through step 3a naturally: when the session plan specifies `subagent_type: "docs-writer"` (project-level) or `subagent_type: "session-orchestrator:docs-writer"` (plugin-level), the resolution chain matches at step 3a without a separate branch. Cross-reference: `agents/docs-writer.md` (agent definition), `skills/docs-orchestrator/SKILL.md` (execution protocol and hook points). No new resolution branch is required — 3a handles it.
535
-
536
- 4. **Finalization** → direct execution (no subagent needed)
537
-
538
- > **How to detect project agents:** The session plan's "Agent Registry" section lists all discovered agents. If an agent name does NOT contain a colon (`:`), it's a project-level agent. If it contains `session-orchestrator:`, it's a plugin agent. If it contains `cursor:` (e.g. `cursor:composer-2.5`), it is a **foreign-channel dispatch**, not an Agent-tool dispatch at all — see § Third branch: foreign-model dispatch. Any other prefix is rejected at parse time by `scripts/lib/config.mjs` (known channels: `cursor`, `session-orchestrator`), so an unroutable value never reaches this chain.
539
-
540
- **`run_in_background: true` is ALLOWED and RECOMMENDED for wave dispatch.** Measured 2026-08-22 (v2.1.239): under blocking dispatch the coordinator was 143 s incapable of acting between an agent's mid-run escalation and its own next turn — escalation latency equals the batch's remaining runtime. Background dispatch returns turns to the coordinator between agent completions; a running agent received a queued message mid-run and answered ~9 min before its final report. Still dispatch in small batches of 3–4 Agent() calls per message (never a large single-message fan-out — see § Dispatch Agents; large fan-outs drop calls silently, conf 1.0), then run **Started-Set Verification** — an agent counts as started on its `meta.json` sidecar and as completed on its task-notification, never on the launch ack.
541
-
542
- #### Platform-Specific Dispatch
543
-
544
- **Claude Code:** Use the `Agent` tool as shown above. Agent types follow the resolution chain above.
545
-
546
- **Codex CLI:** Codex uses typed agent roles defined in `.codex-plugin/agents/`. Map wave roles to Codex agents:
547
- - **Discovery** waves → `explorer` agent (read-only)
548
- - **Impl-Core / Impl-Polish** waves → `wave-worker` agent (workspace-write), or project-specific agents if defined in the platform's agents directory (`.claude/agents/`, `.codex/agents/`, or `.cursor/agents/`)
549
- - **Quality** review → `session-reviewer` agent (read-only)
550
- - **Finalization** → direct execution (no subagent needed)
551
-
552
- Dispatch via Codex's multi-agent system — describe the task and specify the agent role. The prompts remain identical across platforms.
553
-
554
- **Cursor IDE:** No Agent() tool available. Execute wave tasks sequentially within the current Composer session:
555
- 1. For each task in the wave, implement it fully (you are both coordinator AND implementer)
556
- 2. After completing each task, report status inline
557
- 3. Run incremental quality checks after all tasks in the wave complete
558
- 4. Proceed to the next wave
559
-
560
- The `agents-per-wave` config is ignored on Cursor — all work is sequential. Session-reviewer dispatch is deferred to session-end (Phase 1.8).
561
-
562
- > **Timeout note:** Agent timeout is controlled by `maxTurns` from `circuit-breaker.md`, not by a time-based timeout. Claude Code's built-in turn limit provides the safety net. There is no need to set explicit time-based timeouts on agent dispatch.
563
- >
564
- > **Foreign-dispatch exception (#1150):** this is FALSE for a `cursor:<model>` dispatch. `cursor-agent` has no `maxTurns` and no turn limit of any kind, so a wall-clock SIGTERM is the **only** circuit breaker there — `timeoutSec` (default `DEFAULT_TIMEOUT_SEC = 900`, a measured floor, not a suggestion: `cursor-grok-4.6-high` ran 2 of 3 hard-test tasks past a 540 s cap). Lowering it manufactures timeouts that read as model failure. See § Third branch: foreign-model dispatch.
565
-
566
- ### 2. Review Agent Outputs
567
-
568
- **Step 2.0 — Restore coordinator CWD (#219):** BEFORE reading any agent output or running any quality check, restore the coordinator's working directory. Claude Code's `Agent` tool with `isolation: "worktree"` `chdir()`s into each worktree internally and does NOT restore it on agent return. Subsequent Edit/Write/Bash calls would silently route to whichever worktree's tree CWD last drifted into.
569
-
570
- ```js
571
- import { restoreCoordinatorCwd } from '$PLUGIN_ROOT/scripts/lib/worktree.mjs';
572
-
573
- const cwd = await restoreCoordinatorCwd();
574
- if (cwd.restored) {
575
- console.warn(`wave-executor: restored coordinator CWD from ${cwd.from} → ${cwd.to}`);
576
- // Include this line in the wave progress update so the coordinator has an audit trail.
577
- }
578
- ```
579
-
580
- Run this step for every wave, regardless of isolation setting — it is a no-op when CWD never drifted.
581
-
582
- **Step 2.0-bis — Transcript tailer (FA-1, #1114):** `monitors/monitors.json` carries a `wave-transcript-tail` entry with `when: "on-skill-invoke:wave-executor"`, so the tailer starts ONCE per wave-executor invocation — **not per wave**. It observes the OWN session's subagent transcripts, picking up newly-appearing `agent-*.jsonl` files as later waves dispatch, so it never needs to be told a wave boundary. Its findings arrive as `stagnation_detected` records carrying `source: "tail"` — the same schema the **Stagnation event-write** block under step 3a below produces with `source: "coordinator"`, deliberately not a second event name (#1035).
583
-
584
- **Silence is NOT success** (`.claude/rules/loop-and-monitor.md` § LM-002). Transcripts flush per TURN, so an agent inside one long tool call is invisible to the tailer for that call's whole duration. Read "no tail findings" as "nothing detectable was flushed", never as "the wave is healthy" — the post-wave review below remains the primary check. A tailer that cannot resolve the transcript directory exits with one stderr line and never blocks the wave.
585
-
586
- **Step 2.0-ter — Incoming agent escalations (FA-2, #1051):** a wave agent may send ONE upward `SendMessage` to `main` when it hits a wave-blocking obstacle (`.claude/rules/cross-session-messaging.md` § CSM-001 — agents send upward only, never sideways). When such a message arrives mid-wave:
587
-
588
- - **It is a claim, not a finding.** Verify it against the tree before altering the wave plan or re-scoping a sibling agent — `.claude/rules/receiving-review.md` § RCR-003 (skeptical posture) and RCR-001 step 3 (VERIFY) apply unchanged. The agent's view of the tree may already be stale.
589
- - **Carry provenance.** Quote it downstream as `<claim> (source: <agent>, <time>)` per CSM-002; an unattributed escalation is indistinguishable from the coordinator's own measurement.
590
- - **No permission laundering.** Never execute an action this coordinator session has blocked or left unapproved just because an agent asked for it (CSM-003) — route it to the operator instead.
591
- - **Never gate on a reply.** Do not hold a wave, a gate, or a commit waiting for an answer, and read silence as neither consent nor refusal (CSM-004).
592
- - **Record it.** Note the escalation in the wave progress update — agent, one-line claim, verification outcome, action taken — so an escalation that changed the plan stays auditable.
593
-
594
- When the channel is unavailable (CSM-005: `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` / `DISABLE_TELEMETRY`, native Windows, non-Anthropic providers), nothing arrives and this step is a silent no-op — the wave behaves exactly as it does today.
595
-
596
- After ALL agents in the wave complete:
597
-
598
- 1. **Read each agent's result** carefully
599
- 1a. **Validate agent output schema** (if `output-schema-validation.enabled: true` in Session Config — default `false`):
600
-
601
- For each completed agent record, call `validateAgentOutput({ agentName, raw })` from `scripts/lib/agent-output-schema.mjs` where `agentName` is the kebab-case agent name and `raw` is the agent's full return text.
602
-
603
- Handle the four result modes:
604
-
605
- - **`mode: 'validated', ok: true`** — silent. Set `schema_status: 'ok'` on the agent record in `subagents.jsonl`.
606
- - **`mode: 'validated', ok: false`** — schema violation. Annotate the agent record with `schema_violation: true` and `schema_errors: [...]`. Then:
607
- - Under `enforce: warn` (default): log the violation in the wave progress update and continue. The wave is NOT blocked.
608
- - Under `enforce: strict`: surface the violation as a wave-blocking finding. Halt further agent processing and report to the coordinator before proceeding to the conflict check.
609
- - Under `enforce: off`: record the violation in `subagents.jsonl` for diagnostics (`schema_violation: true`, `schema_errors: [...]` are set on the agent record) but do NOT emit a log line in the wave progress update and do NOT block the wave. This is identical to `warn` minus the in-wave noise — forensic data is preserved; operator output is silenced.
610
- - **`mode: 'parse-error'`** — two distinct diagnostic sub-cases collapsed into one mode for backward-compat; either:
611
- - **parse-error (no-block)**: agent output contains no fenced ```json block at all. Common backward-compat case for agents that predate the schema contract.
612
- - **parse-error (bad-json)**: a fenced ```json block exists but the block fails `JSON.parse`. Indicates an agent-side serialisation bug — more interesting than no-block from a diagnostic standpoint, and the operator may want to follow up.
613
-
614
- Both sub-cases share the same recovery: log a warning in the wave progress update, set `schema_status: 'parse-error'` on the agent record in `subagents.jsonl`, and do NOT block the wave (#474 LOW-8 distinguishes the two so future tooling can route diagnostics differently per sub-case).
615
- - **`mode: 'schema-error'`** — the fenced ```json block parses cleanly but the parsed object fails AJV validation against the agent's declared `output-schema:`. This is a stronger signal than `parse-error`: the agent emitted JSON, but the shape diverged from its declared contract. Treat the same way as `validated, ok: false` under the configured `enforce` level (`warn` / `strict` / `off`) so the violation is recorded with `schema_violation: true` and `schema_errors: [...]`. Note: the legacy `validateAgentOutput()` returns `'validated', ok: false` for this case today — `schema-error` is the spec-level name (per #474 LOW-8) for the same condition, kept distinct from `parse-error` so the diagnostic log can route differently.
616
- - **`mode: 'unvalidated'`** — the agent has no declared `output-schema:` frontmatter. Silent skip (backward-compat path; as of #449 all 11 plugin agents are enrolled, but third-party agents installed via marketplace plugins may not be).
617
-
618
- Reference: agent contract at `agents/code-implementer.md`; runtime module at `scripts/lib/agent-output-schema.mjs::validateAgentOutput`.
619
-
620
- 2. **Check for conflicts**: did two agents modify the same file? → manual merge needed
621
- 3. **Check for failures**: did any agent report errors or blockers?
622
- 3a. **Apply stagnation patterns** (per agent): review each agent's tool-call sequence against the three patterns in `circuit-breaker.md` § Stagnation Patterns — Pagination Spiral, Turn-Key Repetition, Error Echo. Mark each agent STAGNANT/SPIRAL/FAILED accordingly; recovery feeds into step 3 (Adapt Plan). Two different agents reading the same file is coordination, not stagnation. The other two patterns in the enum — `psa007-git-write` and `status-partial` — are NOT yours to judge here: they are executable regexes owned by the tailer (step 2.0-bis) and reach you as records with `source: "tail"`.
623
-
624
- **Stagnation event-write** (gated on `persistence: true`): when any stagnation pattern fires for an agent during this step, emit ONE `stagnation_detected` record through the **canonical emitter** — never a hand-rolled `>>` append. Hand-written appenders drift from `emitEvent()` (that drift is what produced the `stop` vs `orchestrator.session.stopped` divergence, #609/#611), and since #1114 this event has a SECOND producer (the transcript tailer, step 2.0-bis above), whose records must be field-for-field comparable with the coordinator's. One write path for both:
625
-
626
- node "$PLUGIN_ROOT/scripts/emit-event.mjs" --type stagnation_detected --payload '<the payload object below>'
627
-
628
- From a Node context, call `emitEvent('stagnation_detected', { ...payload, ...sessionAttribution(repoRoot) }, { repoRoot })` from `scripts/lib/events.mjs` instead — pass `repoRoot` **explicitly** so the record lands in THIS working copy's ledger and its attribution is read from the same root the line is pinned to (#941/#1147).
629
-
630
- ```json
631
- {"session":"<semantic session id>","wave":N,"agent":"<subagent_type>","pattern":"pagination-spiral|turn-key-repetition|error-echo","source":"coordinator","error_class":"<taxonomy value — omit field entirely unless pattern is error-echo>","file":"<relative path from project root, or null if not applicable>","occurrences":N}
632
- ```
633
-
634
- The template lists only the THREE patterns you may write. The other two enum values — `psa007-git-write` and `status-partial` — are tail-only: they are emitted by `scripts/lib/wave-transcript-tail.mjs` with `source: "tail"`, never by the coordinator (see step 3a above).
635
-
636
- `timestamp` and `event` are written by `emitEvent()` itself — do NOT hand-compose either (a hand-typed ISO string is the #540 corruption class).
637
-
638
- **Field-name reconciliation (three session keys, none redundant):** `session` is the **SEMANTIC** session id — the one that matches `sessions.jsonl.session_id` (measured 2026-08-25: `"main-2026-08-24-session-1"`), which is the join key every consumer reads (`skills/session-end/metrics-collection.md` filters `.session == $sid` with `$sid = $SESSION_ID`, the semantic id). Writing a raw UUID here would produce a record that joins to nothing. `sessionAttribution(repoRoot)` additionally contributes the `session_id` / `semantic_session_id` pair, which is OMITTED rather than fabricated when no `session.lock` is readable — so `session` is the field a consumer may rely on, and the pair is additive provenance. Keep all three; do not collapse them into one.
639
-
640
- **`source` (additive, #1114)** names WHO detected the pattern: `"coordinator"` for a record written here from post-wave review, `"tail"` for one written by `scripts/lib/wave-transcript-tail.mjs`. A consumer that does not know the field behaves exactly as before.
641
-
642
- Assign `error_class` using the taxonomy defined in `circuit-breaker.md` § "3. Error Echo" → Error-Class Taxonomy. That assignment applies to **error-echo only** and is unchanged. Omit the field entirely for every other pattern — `pagination-spiral`, `turn-key-repetition`, and the two tail-detected patterns `psa007-git-write` / `status-partial` carry NO `error_class`, and an absent field means "no class applies", never `"other"`. Paths are relative to the project root. `occurrences` is the count of pattern repetitions detected — minimum 3 for the three threshold-based patterns; `psa007-git-write` and `status-partial` fire on the FIRST occurrence, so `occurrences: 1` is valid for those two.
643
-
644
- 3b. **Worktree base-ref freshness check (#195)**: For each agent dispatched with `isolation: "worktree"` in this wave, verify that the coordinator has not advanced `main` past the worktree's base commit before the merge-back copies files. Call `checkWorktreeBaseRefFresh({ suffix, targetBranch: 'main', agentScope, cwd })` from `scripts/lib/worktree-freshness.mjs`:
645
-
646
- - `decision: 'pass'` (baseSha === currentSha) → proceed with merge-back.
647
- - `decision: 'warn'` (main advanced, no agent-scope overlap) → proceed, but log the drift in the wave progress update so the coordinator can audit. This is typically benign — coordinator commits to unrelated files.
648
- - `decision: 'block'` (main advanced, drift files overlap the agent's scope) → **STOP** the merge-back for this agent. The agent's copy would silently overwrite coordinator-committed work (this is exactly the 2026-04-20 07:30 and 09:00 regression). Either: (a) run `git diff main..wt-branch -- <overlap-files>` and manually reconcile before committing, or (b) ask the user whether to rebase the agent's branch onto current main and retry the merge. Do NOT proceed automatically.
649
- - `decision: 'no-meta'` (meta file missing or corrupted) → log a warning and fall back to manual diff review before commit. Missing meta usually means the worktree was created by an older plugin version; corrupted meta warrants an issue.
650
-
651
- Skip the check entirely for agents dispatched with `isolation: "none"` — there is no worktree merge-back in that path.
652
-
653
- Log every non-`pass` result as an event to `.orchestrator/metrics/events.jsonl` (gated on `persistence: true`):
654
- ```json
655
- {"event":"freshness_check","timestamp":"<ISO 8601 UTC>","session":"<session_id>","wave":N,"agent":"<description>","suffix":"<worktree suffix>","decision":"pass|warn|block|no-meta","drift_commits":N,"overlap_files":M}
656
- ```
657
-
658
- 3c. **File-level grounding** (per wave, informational, gated by `grounding-check: true` — default): compute Planned (union of agent file scopes for this wave from the dispatch metadata) vs Actual (files actually edited by this wave's agents). Report scope creep (Actual ∖ Planned) and incomplete coverage (Planned ∖ Actual). Does NOT block the next wave. Reuses the semantics defined in `skills/session-end/plan-verification.md` § 1.1a — the session-end variant computes against `$SESSION_START_REF`, the per-wave variant computes against the wave's pre-dispatch HEAD snapshot. Not to be confused with pre-dispatch grounding injection (§ Pre-Dispatch Grounding Injection above): that feature is per-agent and runs before dispatch to prevent friction; this check is per-wave and runs after dispatch to detect scope creep. Skip the entire check when `grounding-check: false`.
659
-
660
- 3d. **Edit-Persistence Verify (#724 C5c)** (per agent, blocking on violation): an agent's `STATUS: done` / `STATUS: partial` is a *claim*, not evidence — fleet evidence shows agents reporting a successful Edit whose change never landed on disk (worktree merge-back drop, silent Edit no-op, or a mid-turn abort after the tool-result). Before trusting any agent's output, verify each declared file actually changed on disk.
661
-
662
- For each agent that reported `done` or `partial`, take its declared `files_changed` list (from the agent's machine-readable output block, or the "Files changed" section of its prose report) and confirm every declared path appears in the working-tree change set:
663
-
664
- ```bash
665
- # Union of committed-since-dispatch + still-uncommitted changes. Run from repo root.
666
- git diff --name-only "$WAVE_PREDISPATCH_HEAD"..HEAD # files committed during the wave (e.g. auto-commit)
667
- git status --porcelain # files modified / staged / untracked right now
668
- ```
669
-
670
- Build the on-disk change set as the UNION of the two commands' outputs (untracked files appear as `??` lines in `git status --porcelain` — strip the two-column status prefix). **Every path in an agent's declared `files_changed` MUST appear in that union.** A declared file that is absent from both is an **edit-persistence violation**:
671
-
672
- - Treat that agent's result as **NOT verified** — do not count its claimed work as done, and do not feed its (phantom) changes into the next wave.
673
- - **Recover** by either (a) re-dispatching that agent's task package in a fresh batch (per `#### Started-Set Verification`), or (b) applying the missing edit coordinator-direct when the fix is small and unambiguous.
674
- - **Log the deviation** to `## Deviations` in `<state-dir>/STATE.md` via `appendDeviationOnDisk(repoRoot, isoTimestamp, message)` from `scripts/lib/state-md.mjs`:
675
- ```
676
- - [<ISO 8601 UTC>] Wave N edit-persistence violation: agent "<description>" reported <done|partial> but declared file(s) <paths> are absent from the on-disk change set. Result treated as unverified — <re-dispatched | coordinator-direct fix>.
677
- ```
678
-
679
- Cross-reference `.claude/rules/verification-before-completion.md` § VBC-004 Exception 2: a subagent's `STATUS: done` is a claim that needs its own verification — this step is that verification for the file-write side effect. `$WAVE_PREDISPATCH_HEAD` is the HEAD snapshot captured before this wave dispatched (same snapshot used by `### 3c. File-level grounding`). When `persistence: false` (no STATE.md), still perform the check and surface any violation in the wave progress update; only the deviation-write is skipped.
680
-
681
- 3e. **Collect Open Questions** (Close Handover-Alignment-Gate, PRD 2026-07-07): scan every completed agent's report from this wave for an optional `OPEN-QUESTIONS:` line (see the report-line convention in `#### Agent-Type Resolution` above — an agent MAY emit `OPEN-QUESTIONS: <question> | context: <...> | candidates: <opt A / opt B>`; most agents emit none). For each such line found:
682
-
683
- - Parse the question text (portion before the first ` | `).
684
- - Dedup across this wave's agents by question text (case-sensitive exact match after trim) — if two agents raised the same question, keep one.
685
- - Assign `source: 'W<N>/<agent-description-or-subagent_type>'` (the wave number + the reporting agent) and a `priority` — default `medium` unless the agent's report text contains an explicit priority hint ("high priority" / "blocking" → `high`; "low priority" / "nice to know" → `low`).
686
-
687
- The resulting deduped list feeds `### 3a. Post-Wave: Update STATE.md` step 6 (`## Open Questions`), which does the actual lock-guarded `appendOpenQuestionOnDisk` write. This step (3e) only collects and dedups in-memory — it performs no STATE.md I/O itself, the same division of labor as steps 2/3 above (detect here, write in the Post-Wave STATE.md update). Skip entirely when no agent in the wave emitted an `OPEN-QUESTIONS:` line.
688
- 4. **Run incremental verification** (per the quality-gates skill, based on the wave's role):
689
-
690
- **Shared-lib touch auto-promotion (#555 FL-3)** — before selecting the role-based gate variant below, check whether this wave touched files under `scripts/lib/`, `hooks/`, or `.husky/`. If so, auto-promote the inter-wave gate from Quality-Lite (Incremental) to Full Gate (typecheck + test + lint). Rationale: an Impl wave that touches shared code has a wider blast radius than the agent can predict — deep-1647 inter-wave 3→4 caught 2 such regressions only because the Lite step happened to run the full test suite. Auto-promotion makes that coverage deterministic without imposing per-session cost on waves that don't touch shared code (W1-D5 chose Option B over the always-full Option A on this exact tradeoff).
691
-
692
- ```js
693
- import { detectSharedLibTouch } from '$PLUGIN_ROOT/scripts/lib/quality-gate.mjs';
694
-
695
- const touchResult = detectSharedLibTouch({
696
- repoRoot: process.cwd(),
697
- sinceRef: SESSION_START_REF,
698
- promoteWhenTouched: ['scripts/lib/', 'hooks/', '.husky/'],
699
- });
700
-
701
- if (touchResult.touched && (waveRole === 'Impl-Core' || waveRole === 'Impl-Polish')) {
702
- console.log(
703
- `ℹ Quality-Lite auto-promoted to Full Gate — wave touched shared code: ` +
704
- `${touchResult.paths.join(', ')} (#555 FL-3)`,
705
- );
706
- // Run Full Gate (typecheck + test + lint) instead of the role-default Incremental.
707
- } else {
708
- // Existing role-based selection (Discovery: none, Impl-*: Incremental, Quality: Full, Finalization: git status).
709
- }
710
- ```
711
-
712
- `detectSharedLibTouch` never throws — on any git failure (invalid sinceRef, detached HEAD, missing repo) it returns `{ touched: false, paths: [] }`, so a probe failure silently falls back to the role-default Incremental rather than blocking the wave. When `waveRole === 'Quality'`, the gate is **already Full** — no further promotion possible, no double-promotion. When `waveRole === 'Discovery'` or `'Finalization'`, this check is skipped entirely (the role's verification semantics don't include a test gate to promote).
713
-
714
- **Baseline cache check (#258, #724)** — before running Incremental quality checks for this wave, consult the session-start Baseline cache. If the cache is still valid and the diff since `$SESSION_START_REF` is narrow (<50 files), skip Incremental for this wave and note the skip in the wave progress update. **The Quality wave is exempt from the skip**: pass the current wave's `waveRole` so `shouldSkipIncremental` hard-returns `skip: false` (reason `quality-wave-full-gate-mandate`) BEFORE any cache/diff logic runs — the Quality-wave Full Gate is mechanically un-skippable (#724 C6).
715
-
716
- ```js
717
- // import at the top of the wave-executor runtime
718
- import { shouldSkipIncremental } from '$PLUGIN_ROOT/scripts/lib/quality-gates-cache.mjs';
719
-
720
- // waveRole is this wave's role: Discovery | Impl-Core | Impl-Polish | Quality | Finalization.
721
- // When waveRole === 'Quality', shouldSkipIncremental hard-returns skip=false so the Full Gate
722
- // ALWAYS runs — the cache short-circuit applies only to the Impl waves.
723
- const skip = shouldSkipIncremental({ repoRoot: process.cwd(), sessionStartRef: SESSION_START_REF, waveRole });
724
- if (skip.skip) {
725
- console.log(`ℹ Incremental quality check skipped — ${skip.reason} (${skip.changedFileCount} files changed).`);
726
- // proceed to next wave without running Incremental
727
- } else {
728
- // run the role-specific quality check as before (per role-specific rules below).
729
- // For the Quality wave, skip.reason === 'quality-wave-full-gate-mandate' and the Full Gate runs.
730
- }
731
- ```
732
-
733
- `shouldSkipIncremental` never throws — on any error (git failure, unreadable cache) it returns `skip: false` so Incremental runs. Full Gate at session-end is NEVER skipped, and after the Quality wave is likewise NEVER skipped — as of #724 the Quality-wave mandate is enforced MECHANICALLY via the `waveRole` parameter (not prose): see the close-safety invariant in `skills/quality-gates/SKILL.md § Baseline Cache (#258)`.
734
-
735
- - After **Discovery**: no verification needed (read-only)
736
- - After **Impl-Core**: Incremental quality checks per quality-gates (test changed files, typecheck)
737
- - After **Impl-Polish**: Incremental quality checks + integration verification
738
- - **Simplification pass** (at the start of the Quality wave, before test/review agents):
739
- 1. Identify all files changed in this session: `git diff --name-only $SESSION_START_REF..HEAD`
740
- 2. Partition the list into **production files** (exclude `*.test.*`, `*.spec.*`, `__tests__/`) and **test files** (exactly that excluded set). Both branches below are independent: skip a branch when its partition is empty; skip the pass entirely only when BOTH partitions are empty — then proceed directly to test/review agents.
741
- 3. Dispatch 1-2 simplification agents with:
742
- - Changed file list (production files only — exclude `*.test.*`, `*.spec.*`, `__tests__/`)
743
- - Reference: `slop-patterns.md` from the discovery skill directory — include the actual patterns in the agent prompt
744
- To include the patterns: read `skills/discovery/slop-patterns.md` and paste the full content into the agent prompt under a "## Slop Patterns Reference" heading. Do NOT ask the agent to read the file itself — include it inline so the agent has zero-dependency context.
745
- - Reference: project's CLAUDE.md (or AGENTS.md on Codex CLI) conventions
746
- - Instruction: "Review each changed file for AI-generated code patterns. Apply targeted simplifications: remove unnecessary try-catch around non-throwing operations, delete over-documentation (params that repeat the name, returns that say 'the result'), replace re-implemented stdlib functions with standard alternatives, simplify redundant boolean logic (if/else returning true/false, double negation, explicit boolean comparisons). Do NOT change functionality. Do NOT touch files you weren't given. Do NOT commit."
747
- - Tools: Read, Edit, Grep, Glob
748
- - Model: sonnet
749
- 4. **Test-consolidation branch** — in the SAME dispatch round as step 3, dispatch exactly 1 test-consolidation agent with:
750
- - File list: the test partition from step 2 (this session's changed test files) plus their immediate neighbours (sibling test files covering the same module — resolve via the production file's basename, e.g. `foo.mjs` → `tests/**/foo*.test.mjs`)
751
- - Instruction: "Consolidate this test corpus. (a) Merge duplicated tests that differ only in input/expected values into ONE parameterized test (table-driven / `it.each`). (b) DELETE any test that fails the falsification check — ask for each test: *would this test go RED if a real bug were introduced in the code it claims to cover?* If no, it catches nothing; remove it. (c) DELETE getter/setter tests, framework-behaviour tests, and prose-presence tests (assertions that a doc/skill file merely CONTAINS a phrase) — see `.claude/rules/testing.md` § 'Test Quality — False-Positive Prevention' and § 'When NOT to Write Tests'. Do NOT touch production files. Do NOT commit."
752
- - **Contract**: the set of bugs the suite catches may only stay the same or GROW. Never delete a test that is the sole falsifier of a real behaviour — when in doubt, keep and report it. Deletions are a SUCCESS outcome, not a regression: a net-negative test LOC with an unchanged bug-catch set is the intended result of this branch.
753
- - **Report**: the agent MUST emit `test_delta: {added, removed, consolidated, net_loc}` in its report so the coordinator can record the pass's effect.
754
- - Tools: Read, Edit, Grep, Glob
755
- - Model: sonnet
756
- 5. After the simplification and test-consolidation agents complete, proceed to Quality test/review agents
757
- - **Review panel = primary bug-catch mechanism (Quality wave)**: the Quality wave's central verification instrument is a multi-persona review panel — `security-reviewer`, `qa-strategist`, `architect-reviewer` — dispatched read-only (Read/Grep/Glob, no Edit/Write) and scoped to the FULL session diff `$SESSION_START_REF..HEAD`, not to a single wave's file scope. Test-writing in this wave is need-gated, not default (see `SKILL.md` § "Agent Prompt Best Practices" point 5): an agent writes a test only for a bug it can name.
758
- Rationale — 2026-07 evidence: the HIGH/MED product bugs actually caught in this repo's sessions came from panel review (argument injection in a base-branch value, a fail-open config gate, a never-wired max-proposals cap, a glob-metacharacter bypass), not from growth of the test corpus. Panel breadth over the full diff also catches coordinator-written code, which per-wave agent scopes never cover.
759
- - After **Quality**: Full Gate quality checks per quality-gates (typecheck + test + lint, must all pass)
760
- (Full Gate is NEVER skipped regardless of cache state — this is the close-safety invariant. As of #724 this mandate is MECHANICAL, not prose-only: the Baseline cache check above passes `waveRole: 'Quality'`, so `shouldSkipIncremental` hard-returns `skip: false` before any cache/diff logic. A targeted/incremental pass is necessary but NOT sufficient — the Quality-wave completion requires the full typecheck + test + lint run.)
761
- - After **Finalization**: final git status check
762
-
763
- #### Auto-Fix Protocol (#521)
764
-
765
- When `verification-auto-fix.enabled: true`, the inter-wave Quality-Gate uses
766
- `runQualityGateWithRetry()` to dispatch up to `max-retries` (default 2)
767
- fixer-agent attempts before aborting.
768
-
769
- Per attempt:
770
- 1. Run quality-gate (lint, typecheck, test in order).
771
- 2. On failure, collect: failure output, corrective_context from
772
- `.orchestrator/current-session.json`, changed files since last green SHA.
773
- 3. Dispatch code-implementer fixer-subagent with the bundle.
774
- 4. Re-run quality-gate.
775
- 5. After max-retries → write `.orchestrator/metrics/verification-failures/<ts>.json`
776
- diagnostics bundle and abort the wave.
777
-
778
- See `SKILL.md` § "Inter-Wave Quality-Gate (with Auto-Fix Loop — #521)" for
779
- the full invocation pattern.
780
-
781
- ##### STATE.md Deviation — Auto-Fix Result
782
-
783
- After `runQualityGateWithRetry()` returns:
784
-
785
- - **If `result.ok === true`:** No deviation entry — quality gate passed, wave proceeds normally.
786
- - **If `result.attempts > 1` and `result.ok === true`:** Append ONE entry to `## Deviations` in `<state-dir>/STATE.md`:
787
- ```
788
- - [<ISO 8601 UTC>] Wave N auto-fix succeeded after N attempts (max-retries config: M). Failed gate(s): <gate-names>. Final pass on attempt N.
789
- ```
790
- - **If `result.ok === false`:** Append ONE entry to `## Deviations` in `<state-dir>/STATE.md`:
791
- ```
792
- - [<ISO 8601 UTC>] Wave N auto-fix exhausted retries after N attempts (max-retries config: M). Failed gate: <gate-name>. Diagnostics bundle: <bundlePath>. Coordinator to review bundle and decide: fix manually, disable auto-fix and retry, or abort wave.
793
- ```
794
-
795
- Use `appendDeviationOnDisk(repoRoot, isoTimestamp, message)` from `scripts/lib/state-md.mjs`.
796
- This is a **coordinator-only** write — fixer-subagents do not write STATE.md. The lock library
797
- ensures atomicity if multiple coordinator-level deviations land in the same wave.
798
-
799
- #### Auto-Commit Checkpoint (Optional, Opt-In)
800
-
801
- > Gate conditions — ALL of the following must be true for this step to run:
802
- > 1. `$CONFIG["auto-commit-per-wave"] === true`
803
- > 2. `$CONFIG.persistence === true`
804
- > 3. The Incremental quality check in step 4 returned **PASS** (skip or fail → do not commit)
805
- > 4. Worktree base-ref freshness check (step 3b) returned **pass** or **warn** for all agents (not **block**)
806
- > 5. No unresolved merge conflicts in the working tree (`git status --short` shows no `UU`/`AA`/`DD` lines)
807
- >
808
- > When any condition is false, skip this step silently. Log "auto-commit-per-wave skipped" in the wave progress update if the gate condition was `auto-commit-per-wave: true` but another condition failed — so the operator knows the flag is set but the checkpoint did not fire.
809
-
810
- **Commit message format:**
811
-
812
- ```
813
- chore(wave-N): auto-checkpoint — <Role> wave complete
814
-
815
- Quality-Lite: PASS | Wave: N / <total-waves> | Session: <session_id>
816
- Agents: <done>/<total> done, <partial> partial, <failed> failed
817
- ```
818
-
819
- **Env-var bypass:** `SO_SKIP_AUTO_COMMIT=1` disables the commit for the current shell invocation regardless of config — useful for CI environments or when a human is reviewing changes mid-session.
820
-
821
- **STATE.md deviation logging:** after a successful commit, append one entry to `## Deviations` using `appendDeviationOnDisk(repoRoot, isoTimestamp, message)` from `scripts/lib/state-md.mjs` (acquires the lock automatically):
822
-
823
- **Wrapper choice:** the canonical on-disk wrapper is `appendDeviationOnDisk(repoRoot, isoTimestamp, message)` — it acquires the STATE.md lock automatically before reading + writing. Callers in `.mjs` modules MUST prefer the on-disk wrapper; callers that pre-read STATE.md contents may use `appendDeviation(stateContents, isoTimestamp, message)` directly but MUST then route the write through `writeStateMd()`. Never use `readFileSync(STATE) → transform → writeFileSync(STATE)` — the race window allows STATE.md corruption under parallel waves (PSA-005).
824
-
825
- ```
826
- - [<ISO 8601 UTC>] Wave N auto-commit: <sha> (<Role>, Quality-Lite PASS, <N> files staged)
827
- ```
828
-
829
- If the commit itself fails (e.g., nothing to commit, pre-commit hook rejects), do NOT append the deviation. Instead, log the failure in the wave progress update as a WARN and continue to the next step without blocking.
830
-
831
- **Mission-status transition:** after a successful auto-commit, transition the mission status for all tasks in this wave from `in-dev` → `testing` using `setMissionStatus(stateContent, taskId, 'testing')` from `scripts/lib/state-md.mjs`. This matches the coordinator-level rule in `SKILL.md § Mission-Status Updates`: "in-dev → testing: Quality wave begins and this item's implementation wave completed without failure." The auto-commit checkpoint fires at the same logical moment — after implementation completes and Quality-Lite passes.
832
-
833
- **Implementation deferred:** This subsection documents the contract. The procedural body (git add/commit sequence + error handling) will land in a future release as `scripts/lib/auto-commit.mjs` (tracked in GitLab #214; not yet implemented as of v3.10.0). Until then, this section is a no-op stub when `auto-commit-per-wave: true` is set; the coordinator MUST warn the user at session-start that auto-commits are not yet active (emit: "auto-commit-per-wave is set but the implementation (scripts/lib/auto-commit.mjs) is not yet available — commits will occur at session-end via /close as normal").
834
-
835
- ---
836
-
837
- 5a. **Persona-reviewer dispatch** (opt-in, gated by `wave-reviewers` config):
838
- - Read `wave-reviewers` from Session Config. If the key is absent or the array is empty → skip this step entirely (no-op).
839
- - Applicable waves: **Impl-Core** and **Impl-Polish** only. Skip for Discovery, Quality, and Finalization waves.
840
- - For each reviewer name in the array, dispatch in parallel with read-only scope. Example:
841
- ```
842
- // Dispatch all configured reviewers in parallel (Promise.all semantics)
843
- Agent({
844
- description: "Persona review — <reviewer-name> — Wave N",
845
- prompt: "<include: wave scope, changed files list, relevant plan section>",
846
- subagent_type: "session-orchestrator:<reviewer-name>",
847
- run_in_background: false // deliberately blocking — see below
848
- })
849
- ```
850
- - **`run_in_background: false` here is deliberate, not an oversight.** Reviewers are dispatched AFTER the quality gate and are NOT in the session plan's agent list, so § Started-Set Verification has no expected-set to check them against and the launch ack — the only thing a background dispatch returns — is explicitly not countable. There is also no consumer for early results: the next step needs ALL reviewer verdicts before it can act (`skills/persona-panel/SKILL.md` § "background dispatch would add turn-juggling with no consumer for early results"; `skills/session-start/SKILL.md` names persona-panel among the keep-false skills).
851
- - Each reviewer writes its findings to `.orchestrator/audits/wave-reviewer-<wave>-<reviewer-name>.md`. The coordinator does NOT need to create this file — the reviewer agent writes it directly.
852
- - **Findings are ADVISORY**: reviewer output never blocks the subsequent wave. After all dispatched reviewers complete:
853
- - If any reviewer reports **WARN**: surface the findings to the user in the wave progress summary. Feed actionable items into the next wave's agent assignments (step 3 — Adapt Plan). If a WARN/FAIL finding is surfaced but NOT converted into a fix task for the next wave, append ONE line to `## Deviations` via `appendDeviationOnDisk(repoRoot, isoTimestamp, message)` (#730/H5): `- [<ISO 8601 UTC>] Wave N reviewer finding overridden (not actioned): <one-line finding>.` — session-end Phase 2.6 (Broken-Window Budget) walks these entries at close.
854
- - If any reviewer reports **FAIL**: surface the findings prominently in the wave progress summary with a `[REVIEWER FAIL]` prefix. Still proceed to step 5 (session-reviewer) — do not halt wave execution.
855
- - If all reviewers report **PASS** or produce no findings: log a one-line note and continue.
856
- - **Default behaviour unchanged**: when `wave-reviewers` is absent or `[]`, this step is a no-op and the wave loop proceeds exactly as before.
857
- - Supported reviewer names (plugin-provided): `architect-reviewer`, `qa-strategist`, `analyst`. Custom reviewer agents in `agents/` are also valid if their `name` frontmatter matches.
858
-
859
- 5. **Session-reviewer dispatch** (after Impl-Core, Impl-Polish, and Quality waves only):
860
- - When integrating reviewer findings, follow the receiving-review protocol — see `.claude/rules/receiving-review.md` for the 6-step pattern (READ → UNDERSTAND → VERIFY → EVALUATE → RESPOND → IMPLEMENT) and the forbidden-phrase list.
861
- - After **Impl-Core** and **Impl-Polish** waves, dispatch the session-reviewer agent to verify wave output:
862
- ```
863
- Agent({
864
- description: "Review wave N output",
865
- prompt: "<include: session plan, wave results, changed files list, acceptance criteria>",
866
- subagent_type: "session-orchestrator:session-reviewer",
867
- run_in_background: false // deliberately blocking — same reason as step 5a
868
- })
869
- ```
870
- - **`run_in_background: false` here is deliberate**, for the same reason as step 5a: the session-reviewer runs after the quality gate, is not in the session plan's agent list (so Started-Set Verification has no signal for it), and its verdict is needed whole before step 3 (Adapt Plan) can consume it.
871
- - The session-reviewer checks changed files against the plan and reports PASS/WARN/FAIL per category (implementation, tests, TypeScript, security, silent failures, test depth, type design, issues).
872
- - If the session-reviewer reports **WARN or FAIL** findings: add fix tasks to the next wave's agent assignments (feed into step 3 — Adapt Plan). If a WARN/FAIL finding is surfaced but NOT converted into a fix task for the next wave, append ONE line to `## Deviations` via `appendDeviationOnDisk(repoRoot, isoTimestamp, message)` (#730/H5): `- [<ISO 8601 UTC>] Wave N reviewer finding overridden (not actioned): <one-line finding>.` — session-end Phase 2.6 (Broken-Window Budget) walks these entries at close.
873
- - After the **Quality** wave: dispatch the session-reviewer with **full session scope** (all files changed since session start, not just the current wave). Use `git diff --name-only $SESSION_START_REF..HEAD` to provide the complete changed files list.
874
- - Include `SESSION_START_REF` (captured in Pre-Wave 1) in the session-reviewer prompt so it can compute the full changed files list independently.
875
- - **Relationship to session-end Phase 1.8:** Wave-level session-reviewer runs provide incremental feedback during execution. Session-end Phase 1.8 runs a final comprehensive review of ALL changes. Both are complementary — wave reviews catch issues early, session-end review is the final quality gate.
876
- - **Discovery** and **Finalization** waves: skip session-reviewer dispatch — Discovery is read-only and Finalization is a final git status check only.
877
- - This is complementary to the incremental verification in step 4 — the session-reviewer provides deeper analysis (security, silent failures, test depth, type design) that automated checks do not cover.
878
- 6. **Pencil design review** (after Impl-Core and Impl-Polish roles only, if `pencil` configured in Session Config):
879
- a. Check Pencil editor state: `get_editor_state({ include_schema: false })`. If no editor active, open the configured `.pen` file via `open_document({ filePathOrTemplate: "<pencil-path>" })`. If that also fails → skip with note "Pencil review skipped — .pen file unavailable."
880
- b. Get design structure: `batch_get({ filePath: "<pencil-path>", patterns: [{ type: "frame" }], readDepth: 2, searchDepth: 2 })` — find frames relevant to this wave's UI work.
881
- c. Screenshot relevant frames: `get_screenshot({ filePath: "<pencil-path>", nodeId: "<frame-id>" })` for each frame matching the wave's UI tasks.
882
- d. Read the actual UI files changed in this wave (from agent outputs).
883
- e. **Compare**: layout structure, component hierarchy, visual elements (headings, buttons, inputs, cards), responsive behavior.
884
- f. **Report** in wave progress:
885
- `- Design: [ALIGNED / MINOR DRIFT / MAJOR MISMATCH] — [specific findings]`
886
- g. **Act on results**:
887
- - ALIGNED → proceed to next wave
888
- - MINOR DRIFT → add fix tasks to next wave (no pause)
889
- - MAJOR MISMATCH → **PAUSE wave execution**:
890
- 1. Report specific mismatches to user
891
- 2. AskUserQuestion: "Continue as-is", "Revise plan for remaining waves", "Abort session"
892
- > If AskUserQuestion is unavailable (Codex CLI), present as numbered list.
893
- 3. If "Revise" → re-run session-plan for remaining waves only
894
- 4. If "Abort" → mark remaining waves as DEFERRED, proceed to session-end
895
-
896
- Always use the `filePath` parameter on Pencil MCP calls. Only review frames relevant to the current wave, not the entire file.
897
-
898
- 7. **Capture wave metrics**: If `persistence` is enabled in Session Config, record for this wave after all agents complete and quality checks run. If `persistence` is `false`, skip metrics capture entirely — do not accumulate in-memory metrics. Record:
899
- - `wave_number`, `role`, `started_at` (when agents were dispatched), `completed_at` (when all finished)
900
- - `agent_count`: number of agents dispatched
901
- - `agent_count_planned`: agents named in the session plan for this wave (Started-Set Verification, #724)
902
- - `agent_count_started`: distinct agents whose `agent-<id>.meta.json` sidecar is present, after any silent-drop re-dispatch (Started-Set Verification, #724/#1115). NOT "produced a tool-result" — under background dispatch the launch ack is a result and would count an agent that never ran. A gap `agent_count_planned > agent_count_started` after re-dispatch signals a persistent silent drop.
903
- - `agent_count_completed`: distinct agents whose task-notification (`<status>completed</status>`) arrived (#1115). A gap `agent_count_started > agent_count_completed` at wave end is an agent that started and never returned — the started-but-never-returned state, not a silent drop.
904
- - Per-agent results: `{description, status: done|partial|failed, files_changed_count}`
905
- - `files_changed`: total unique files changed this wave (from `git diff --stat --name-only`)
906
- - `planned_files_count`: size of this wave's Planned set (union of agent file scopes) as computed in step 3c File-level grounding above. Reuse that value — do not recompute.
907
- - `over_delivery_ratio`: files_changed / max(planned_files_count, 1), rounded to 2 decimals. > 1 = agents touched more files than briefed (under-sizing signal, #730/H4). Omit both fields when `grounding-check: false`.
908
- - `quality_check`: incremental check result (pass/fail/skipped)
909
- - `suite_passed` / `suite_failed` (+ optional `suite_platform`): the full-suite counts feeding the § 3a Wave History header `— suite <passed>/<failed> on <platform>`. `quality_check` is a traffic light; these are the number the light was derived from, and unlike STATE.md (gitignored, demoted to `## Previous Session` and then overwritten) the metrics record survives the session.
910
- **Copy the two counts off the gate's own event — do not re-read them from the terminal (#966 step 3).** `scripts/run-quality-gate.mjs`, the wrapper that fires between waves, emits `orchestrator.quality_gate.{passed,failed}` carrying a machine-measured `counts: {passed, failed, total}` (admitted by `admitSuiteCounts()`) plus the `wave_number` it resolved from `wave-scope.json`. Payload fields are flat at the record's top level:
911
-
912
- ```bash
913
- jq -c --argjson w <wave_number> --arg s "<semantic_session_id>" '
914
- select(.event | startswith("orchestrator.quality_gate."))
915
- | select(.semantic_session_id == $s and .wave_number == $w and .counts != null)
916
- | .counts' .orchestrator/metrics/events.jsonl | tail -1
917
- ```
918
-
919
- The session filter is not optional — `events.jsonl` accumulates across sessions and every past session also had a wave with this number.
920
- **OMIT all three when that selector returns nothing** — absent = "not measured", `suite_failed: 0` = "measured, zero failures". Never write `0` for a suite that did not run. The event enforces the same distinction at the producer: `counts` is omitted, never zero-filled, when the run fail-fast'd before the test gate or its output carried no parseable count.
921
- > **What is NOT on the event, and stays hand-written:** `suite_platform` — the payload has no platform field, so keep writing it from the § 3a header as before. Likewise, the auto-fix-loop producer (`scripts/lib/quality-gate.mjs`, active only under `verification-auto-fix.enabled: true`) emits `counts` WITHOUT `wave_number`, so its retry records correctly never match the selector above; they are mid-wave attempts, not the wave's verdict. If the wave's gate ran outside `run-quality-gate.mjs` entirely, no event exists — fall back to the gate output you read, and say so in the progress update. The reader side (`skills/session-end/metrics-collection.md` § 1.7) reads the event first and this hand-written trio second, so keep writing the trio: it is the compatibility path for those two cases and for sessions already in flight.
922
- Append this wave record to the session metrics `waves` array.
923
-
924
- 7a. **Scope drift tripwire (S2 — #896, warn-only)**: distinct from `over_delivery_ratio` above — that metric is per-wave and unfiltered; this one is session-cumulative (since `session-start-ref`) and filtered through `DRIFT_EXCLUDE_PATTERNS`, so the two numbers are NOT expected to agree. Call `computeDrift()` from the same `scripts/lib/scope-baseline.mjs` module as § 0a Scope Baseline Freeze above. Never blocks — exit code stays 0 and the next wave is dispatched regardless of the result.
925
-
926
- ```js
927
- import { computeDrift } from '$PLUGIN_ROOT/scripts/lib/scope-baseline.mjs';
928
-
929
- const drift = computeDrift({ repoRoot: process.cwd(), threshold: 2.0 });
930
- if (drift.skipped === false && drift.breached) {
931
- console.warn(
932
- `⚠ Scope drift: filesRatio ${drift.filesRatio} (${drift.actualFiles} actual / ${drift.plannedFiles} planned files) ` +
933
- `>= threshold ${drift.threshold} — session has grown beyond its frozen scope baseline.`
934
- );
935
- }
936
- ```
937
-
938
- Include the WARN line verbatim in the wave progress update when `breached` is true — name `filesRatio`, `plannedFiles`, `actualFiles`, and the configured `threshold`, not merely the word "drift". `drift.skipped === true` (`no-state-md`, `unreadable-state-md`, `no-baseline`, `stale-baseline`, or `unresolvable-ref` — see `computeDrift()`'s JSDoc for the precedence order) is silent: no WARN, no progress-update line. `persistence: false` implies `no-state-md`, so this step degrades to a silent no-op in that mode without a separate gate check.
939
-
940
- ### 3. Adapt Plan (if needed)
941
-
942
- After reviewing wave results, decide:
943
-
944
- - **On track**: proceed to next wave as planned
945
- - **Minor issues**: add fix tasks to next wave's agent assignments
946
- - **Major blocker**: propose a revised plan for the remaining waves and present the choice to the user via `AskUserQuestion` (proceed / revise / abort). See `.claude/rules/ask-via-tool.md` — never surface this as an inline prose question.
947
- - **Agent failed**: re-dispatch with corrected instructions in next wave
948
- - **Scope change**: document why, adjust remaining waves, present scope deltas to the user via `AskUserQuestion` (accept / reject / modify).
949
-
950
- **Deviation protocol**: ALWAYS document WHY you deviated from the plan. Log it in a brief note that session-end can reference.
951
-
952
- **User interaction protocol**: Any decision surfaced to the user from this loop — plan revisions, scope changes, recovery-path choice, pause/continue prompts — goes through `AskUserQuestion`. Inline markdown-list choices are a bug; see `.claude/rules/ask-via-tool.md`.
953
-
954
- #### Dynamic Scaling
955
-
956
- After reviewing wave results, adjust the next wave's agent count based on performance signals:
957
-
958
- | Signal | Action | Example |
959
- |--------|--------|---------|
960
- | All agents completed in under 3 minutes wall-clock, no issues | Reduce next wave by 1-2 agents | 6 agents all done in <3m → next wave uses 4 |
961
- | Agent failures or broken code | Add fix agents to next wave (+1-2) | 2 agents failed → next wave gets 2 extra |
962
- | Scope expansion discovered | Scale up next wave | New module found → add agents for it |
963
- | Quality regressions found | Add targeted fix agents | 3 test failures → 3 fix agents next wave |
964
-
965
- **Scaling constraints:**
966
- - Never exceed `agents-per-wave` from Session Config
967
- - Never go below 1 agent per wave
968
- - Log all scaling decisions in the wave progress update
969
- - Record actual vs. planned agent count in wave metrics
970
-
971
- ### 3a. Post-Wave: Update STATE.md
972
-
973
- > Skip if `persistence: false`.
974
-
975
- After each wave completes and before the progress update, update `<state-dir>/STATE.md`:
976
-
977
- 1. **Frontmatter**: set `current-wave` to the just-completed wave number; set `status` to `active` (or `paused` if waiting on user input)
978
- 2. **`## Current Wave`**: replace contents with next wave info — wave number, role, agents to dispatch and count
979
- 3. **`## Wave History`**: append an entry for the completed wave (the `(planned … → actual …, over-delivery …)` parenthetical is omitted when `grounding-check: false`, since the counts are unavailable):
980
- > **Record the SUITE COUNT, not just "gates green" — and name the platform (#944).** The wave line MUST carry the full-suite pass/fail count from the gate that just ran (`<passed>/<failed>`), not merely that typecheck and lint were clean. A deep session on 2026-07-30 logged typecheck/lint/validate-plugin for every wave and no suite count; a test that had been vacuous for its entire life sat red on HEAD through three waves and was found only by the review panel — in a session whose own premise was turning CI from red to green.
981
- >
982
- > **A green gate on one platform is not evidence for another.** That same session's local gate reported 541/541 three times on a tree CI could not build: two tests encoded macOS assumptions (a `TMPDIR` that carries a trailing slash; an `ARG_MAX` that tolerates a 200 KB argv entry). Both passed locally and failed on the Linux runner. When the wave touched anything platform-sensitive — spawn/argv shapes, `os.tmpdir()`, path separators, file modes, `$PATH` lookups of external binaries — say so in the wave line, and treat CI, not the local run, as the verdict.
983
-
984
- ```
985
- ### Wave N — <Role> (planned <P> files → actual <A>, over-delivery <R>) — suite <passed>/<failed> on <platform>
986
- - Agent "<description>": <done|partial|failed> — <files changed> — <1-line note>
987
- - Agent "<description>": <done|partial|failed> — <files changed> — <1-line note>
988
- ```
989
- 4. **`## Deviations`**: if the plan was adapted in step 3, append a timestamped entry:
990
- ```
991
- - [<ISO timestamp>] Wave N: <what changed and why>
992
- ```
993
-
994
- 5. **Heartbeat refresh (#590-3)** — after the STATE.md write, refresh the session-lock heartbeat so long-running deep sessions do not let the 4h TTL lapse between waves. Best-effort: a failure must NOT block the wave.
995
-
996
- ```js
997
- // Per-wave heartbeat refresh (#590-3) — keeps session.lock fresh during long deep sessions.
998
- // sessionId = the session identifier established by session-start Phase 1.2 acquire()
999
- // and stored in .orchestrator/session.lock (session_id field); matches the
1000
- // STATE.md frontmatter `session:` field written during Pre-Wave 1b initialization.
1001
- import { updateHeartbeat } from '../../scripts/lib/session-lock.mjs';
1002
- updateHeartbeat({ sessionId, repoRoot: process.cwd() });
1003
- ```
1004
-
1005
- Skip silently if `persistence: false` in Session Config (no session.lock exists in that mode).
1006
-
1007
- 6. **`## Open Questions`** (Close Handover-Alignment-Gate, PRD 2026-07-07): append the wave's deduped open questions collected earlier in `3e. Collect Open Questions`, via `appendOpenQuestionOnDisk` — the same lock-guarded on-disk pattern used by `appendDeviationOnDisk` above:
1008
-
1009
- ```js
1010
- import { appendOpenQuestionOnDisk } from '../../scripts/lib/state-md.mjs';
1011
- for (const q of dedupedOpenQuestions) {
1012
- await appendOpenQuestionOnDisk(repoRoot, { question: q.question, source: q.source, priority: q.priority });
1013
- }
1014
- ```
1015
-
1016
- Skip silently when the wave produced no `OPEN-QUESTIONS:` lines (see `3e. Collect Open Questions`) and when `persistence: false`.
1017
-
1018
- ### 3a-bis. Agent-Status Telemetry (#565)
1019
-
1020
- > Optional operator-side observability — NOT load-bearing. Best-effort, fire-and-forget telemetry that a tmux `--with-status-pane` (see `skills/tmux-layout/SKILL.md`) renders as a live side-channel per ADR-0007. A status push must NEVER block or fail a wave — mirror the §3a heartbeat-refresh framing exactly.
1021
-
1022
- **Gate:** `persistence: true` in Session Config. When `persistence: false`, skip every push below — there is no runtime side-channel to feed.
1023
-
1024
- The helper is `scripts/lib/agent-status.mjs`. Its exports (`setStatus`, `setProgress`, `readCurrentStatus`) are all no-throw and return `{ ok: true } | { ok: false, reason }`; the coordinator ignores the return value (best-effort). Push at **three anchors** in the wave loop:
1025
-
1026
- 1. **dispatch** — in `### 1. Dispatch Agents`, as each agent is dispatched, push its status. Use `setProgress` when the wave's per-agent ordinal is meaningful, else `setStatus`:
1027
-
1028
- ```js
1029
- import { setStatus, setProgress } from '../../scripts/lib/agent-status.mjs';
1030
-
1031
- // For each agent dispatched in this wave (i = 0-based position, total = wave agent count):
1032
- await setStatus(agentId, `dispatched — ${subagentType}`); // free-text variant
1033
- // — or —
1034
- await setProgress(agentId, { step: i + 1, total, label: subagentType }); // progress variant
1035
- ```
1036
-
1037
- `agentId` is a stable per-agent key (e.g. `wave${waveN}-${i}-${subagentType}`). There is **no separate "agent-start" hook distinct from dispatch** — wave agents are in-process `Agent()` calls with no PID/TTY (see `skills/tmux-layout/SKILL.md § When NOT to Use`), so dispatch IS the start signal. Do not invent one.
1038
-
1039
- 2. **agent-end** — in `### 2. Review Agent Outputs` step 1 (Read each agent's result), as each agent's terminal status is determined, push it:
1040
-
1041
- ```js
1042
- // status ∈ {'done','partial','failed'} from the agent's STATUS: line
1043
- await setStatus(agentId, status);
1044
- ```
1045
-
1046
- 3. **wave-end rollup** — in `### 3a. Post-Wave: Update STATE.md`, beside the `updateHeartbeat` call (step 5), push one wave-level rollup using a wave-scoped key:
1047
-
1048
- ```js
1049
- // e.g. agentId = `wave${waveN}` ; counts from the wave's per-agent results
1050
- await setStatus(`wave${waveN}`, `wave ${waveN} complete — ${done} done, ${partial} partial, ${failed} failed`);
1051
- ```
1052
-
1053
- A push failure (timeout, fs-error, invalid-input) is logged to the wave progress update at most as a one-line WARN — never block, never retry, never surface to the user. If `agent-status.mjs` is absent (older plugin checkout), wrap the import defensively and no-op, exactly as `layouts.mjs` does for its telemetry import.
1054
-
1055
- ### 3b. Persona-Gate Hook (#458)
1056
-
1057
- > Opt-in mid-wave hook that fans out a `/persona-panel`-style review after a configured wave completes. Distinct from `### 5a. Persona-reviewer dispatch` (which uses the `wave-reviewers` Session Config key and dispatches code-oriented `architect-reviewer` / `qa-strategist` / `analyst` agents). This hook uses the `persona-gate-wave` Session Config key and dispatches catalog personas (domain-experts, buyer-personas, auditors) from `.claude/personas/`. The two keys are independent and may both be configured on the same project.
1058
-
1059
- **Gate conditions** — ALL must be true for the hook to fire:
1060
-
1061
- 1. `persona-gate-wave.enabled: true` in Session Config (default: `false`).
1062
- 2. The just-completed wave matches `persona-gate-wave.after` — one of `'quality'` or `'impl-polish'`. The hook runs AFTER step 3a (STATE.md updated) and BEFORE step 4 (progress update), so the dispatch context already reflects the completed wave's results.
1063
- 3. `persona-gate-wave.mode !== 'off'` (when `mode: 'off'` the hook is a silent no-op even when `enabled: true`).
1064
-
1065
- When any gate condition is false, skip this step entirely — proceed to `### 4. Progress Update`.
1066
-
1067
- **Dispatch sequence:**
1068
-
1069
- ```js
1070
- import { loadCatalog } from '$PLUGIN_ROOT/scripts/lib/persona-panel/catalog-loader.mjs';
1071
- import { buildPersonaPrompt, validatePersonaOutput } from '$PLUGIN_ROOT/scripts/lib/persona-panel/persona-runner.mjs';
1072
- import { consolidate } from '$PLUGIN_ROOT/scripts/lib/persona-panel/consolidator.mjs';
1073
- import { writeJsonAtomic } from '$PLUGIN_ROOT/scripts/lib/io.mjs';
1074
- import { appendDeviationOnDisk } from '$PLUGIN_ROOT/scripts/lib/state-md.mjs';
1075
-
1076
- const cfg = $CONFIG['persona-gate-wave']; // already normalised by parseSessionConfig
1077
- const catalog = await loadCatalog(); // throws if .claude/personas/ missing or invalid
1078
- const rosterNames = cfg.personas.length > 0
1079
- ? cfg.personas
1080
- : [...catalog.keys()]; // empty list → all catalog personas
1081
- const personas = rosterNames.map((n) => catalog.get(n)).filter(Boolean);
1082
- ```
1083
-
1084
- Dispatch each persona in parallel via the Agent tool, using `cfg['dispatch-model']` as the model and `Read, Grep, Glob` tools only (panel personas are read-only by contract). Each dispatch wraps the wave's scope summary + changed-files list in `buildPersonaPrompt(persona.persona, target, targetContent)`.
1085
-
1086
- After all agents return, collect their outputs and validate each via `validatePersonaOutput(persona.persona, agentText)`. Compose the panel verdict via `consolidate(outputs, 'hard-gate-threshold', { threshold: cfg.threshold_parsed })`.
1087
- <!-- threshold_parsed is pre-computed by _normalizePersonaGateWave in persona-gate-wave.mjs; no re-parse needed here -->
1088
-
1089
- **Behaviour by mode:**
1090
-
1091
- | `mode` | Action on consolidator result |
1092
- |--------|--------------------------------|
1093
- | `off` | No dispatch (gate condition above). |
1094
- | `warn` | Log findings to the wave progress update under a `Persona-gate:` bullet. Continue to step 4 regardless of `final_verdict`. |
1095
- | `strict` | If `final_verdict === 'PROCEED'`: log to progress, continue. Otherwise pause and surface an `AskUserQuestion` with three options:<br>1. **proceed-as-is** — log Deviation, continue (Recommended only after operator inspects sidecar)<br>2. **revise-remaining-waves** — return `{ verdict: 'FIX_REQUIRED', revision_context: { dissenting_personas, recommendations } }` to the wave-executor caller<br>3. **abort-session** — return `{ verdict: 'BLOCKED' }` to the caller |
1096
-
1097
- **Sidecar write:** before reporting any verdict, validate the panel result against `agents/schemas/persona-panel-sidecar.schema.json` (via `validateAgentOutput` or a direct AJV compile) and then write atomically via `writeJsonAtomic(path, value, { schemaPath })`:
1098
-
1099
- ```
1100
- .orchestrator/persona-panel/<iso-timestamp>-<runId>.json
1101
- ```
1102
-
1103
- The sidecar carries `personas_invoked`, per-persona `outputs`, and the full `consolidation` block — operators consult it from the AskUserQuestion prompt before deciding `strict`-mode follow-up.
1104
-
1105
- **STATE.md deviation contract:** on `warn` (with at least one dissenting persona) or any `strict`-mode non-PROCEED verdict, append one timestamped entry to `## Deviations` via `appendDeviationOnDisk(repoRoot, iso, message)` from `scripts/lib/state-md.mjs` (acquires the STATE.md lock):
1106
-
1107
- ```
1108
- - [<ISO 8601 UTC>] Wave N persona-gate <warn|strict-proceed|strict-revise|strict-abort>: dissenting=[<persona-1>, <persona-2>], threshold=<cfg.threshold>, mode=<cfg.mode>. Sidecar: <relative-path>.
1109
- ```
1110
-
1111
- On a clean `PROCEED` no deviation is written — the sidecar alone is sufficient evidence.
1112
-
1113
- **Wave metrics extension:** when persistence is enabled, extend the wave metrics record (step 7 of `### 2. Review Agent Outputs`) with a `persona_gate` block:
1114
-
1115
- ```json
1116
- "persona_gate": {
1117
- "triggered": true,
1118
- "threshold": "<cfg.threshold>",
1119
- "personas_pass": <N>,
1120
- "personas_fail": <M>,
1121
- "mode_used": "<cfg.mode>",
1122
- "final_verdict": "<PROCEED|PROCEED_WITH_FOLLOWUPS|BLOCKED|REQUIRES_COORDINATOR>",
1123
- "sidecar_path": ".orchestrator/persona-panel/<...>.json"
1124
- }
1125
- ```
1126
-
1127
- When the hook is skipped (gate condition false), omit the `persona_gate` field entirely — never write `triggered: false` for skipped runs, so a downstream consumer can distinguish "hook did not fire" from "hook fired but found no dissent".
1128
-
1129
- **Motivating example:** a flagship product's W5 Buyer-Panel pattern (six buyer personas at `hard-gate-threshold` `6-of-6`, `mode: 'strict'`, `after: 'quality'`) — UI work is gate-checked against every persona before commit, abort on any dissent. See `docs/session-config-reference.md § Persona-Gate Wave (#458)` and `commands/persona-panel.md` for the standalone CLI equivalent.
18
+ Turn budget, `maxTurns`, and stagnation recovery are unmoved: `circuit-breaker.md`.
1130
19
 
1131
20
  ### 4. Progress Update
1132
21
 
@@ -1143,154 +32,8 @@ After each wave, provide a brief status:
1143
32
  - Adaptations for Wave [N+1] ([NextRole]): [none / list changes]
1144
33
  ```
1145
34
 
1146
- ## Scope Manifest
1147
-
1148
- Before each wave dispatch:
1149
-
1150
- 1. **Write `<state-dir>/wave-scope.json`** with the wave's scope:
1151
- > (Platform-specific: `.claude/wave-scope.json` on Claude Code, `.codex/wave-scope.json` on Codex CLI, `.cursor/wave-scope.json` on Cursor IDE)
1152
-
1153
- **Deriving `blockedCommands` (effective floor∪overlay policy, #155/#972):** Before writing `wave-scope.json`, derive the blocked patterns from the EFFECTIVE policy via the shared merge module — the plugin's floor policy united with the repo's overlay policy. (A bare `jq` over the repo-local policy file alone under-counts the merged result since #972.)
1154
- ```bash
1155
- BLOCKED=$(node --input-type=module -e "
1156
- import { loadEffectivePolicy } from '$PLUGIN_ROOT/scripts/lib/blocked-commands-policy.mjs';
1157
- const { rules } = await loadEffectivePolicy({ cwd: process.cwd(), projectDir: process.env.CLAUDE_PROJECT_DIR ?? null, pluginRoot: '$PLUGIN_ROOT' });
1158
- console.log(JSON.stringify((rules ?? []).filter(r => r.severity === 'block').map(r => r.pattern)));
1159
- ")
1160
- ```
1161
- Use `$BLOCKED` as the `blockedCommands` value in `wave-scope.json`. Since #972 this is the effective floor∪overlay policy — identical to what the destructive-guard hook enforces.
1162
-
1163
- **Fallback:** If the command fails or prints `[]` (neither the plugin's floor policy nor a repo policy resolvable — pre-#155 setup), use the legacy hardcoded array and log a warning in the wave progress update:
1164
- ```bash
1165
- BLOCKED='["rm -rf", "git push --force", "DROP TABLE", "git reset --hard", "git checkout -- ."]'
1166
- # Warning: policy file .orchestrator/policy/blocked-commands.json not found — using legacy hardcoded blocklist
1167
- ```
1168
-
1169
- **Deriving the session binding (#1123):** `wave-scope.json` lives in the WORKING COPY, and `hooks/enforce-scope.mjs` applies whatever it finds there to every session running in that checkout. Without a binding, a Discovery wave's `allowedPaths: []` denied every write of an unrelated parallel session. Name the writer — both fields come from ONE `sessionAttribution()` call, which reads `.orchestrator/session.lock` once:
1170
- ```bash
1171
- SESSION_BINDING=$(node --input-type=module -e "
1172
- import { sessionAttribution } from '$PLUGIN_ROOT/scripts/lib/events.mjs';
1173
- const a = sessionAttribution(process.cwd());
1174
- const out = {};
1175
- if (a.session_id) out.session = a.session_id;
1176
- if (a.semantic_session_id) out.semantic_session = a.semantic_session_id;
1177
- console.log(JSON.stringify(out));
1178
- ")
1179
- ```
1180
- **Verify the binding names YOU before you write it (#1123 follow-up).** `sessionAttribution()` reads the repo-global `.orchestrator/session.lock`, which in a shared working copy can hold a PEER's id — a session that lost the acquire race (`bootstrapLock()` reason `active`) leaves the lock naming the session that won it. Compare both ids against your own session (STATE.md `session`): **if they do not match, OMIT the `session`/`semantic_session` keys entirely** and write an unbound manifest. Unbound = ENFORCE, which is the fail-closed direction; writing a foreign id instead publishes a manifest that classifies as somebody else's for every reader. The reader half is defensive against exactly this (Gate 3b resolves identity as the UNION of payload, env and lock, so a peer-owned-lock manifest still reads `own` to its writer), but the reader cannot repair a binding the writer knowingly got wrong — and a manifest naming a peer is unreadable as an audit record either way.
1181
-
1182
- Merge `$SESSION_BINDING`'s keys into the manifest. **If a value is empty, OMIT the key — never write `"session": ""`.** An empty id is present-but-equal-to-nobody: the legacy warning stays silent while every reader compares it against its own id, finds no match, and treats the manifest as FOREIGN — the one disposition that skips enforcement entirely. `sessionAttribution()` already omits rather than fills (CI runs hold no lock), and `validate-wave-scope.mjs` rejects the empty string outright, so the honest path is also the only one that validates.
1183
-
1184
- ```json
1185
- {
1186
- "wave": N,
1187
- "role": "<role>",
1188
- "enforcement": "<from Session Config, default: warn>",
1189
- "session": "<raw session_id from sessionAttribution(); OMIT the key if unavailable>",
1190
- "semantic_session": "<semantic_session_id from the same call; OMIT if unavailable>",
1191
- "allowedPaths": ["<from agent specs in session plan>"],
1192
- "blockedCommands": "<derived dynamically from the effective floor∪overlay policy via loadEffectivePolicy (severity: block rules, #972); falls back to legacy 5-element array if no policy resolves>",
1193
- "gates": "<copy of enforcement-gates from Session Config, or omit if unset>"
1194
- }
1195
- ```
1196
- The `gates` field (optional) mirrors `enforcement-gates` from Session Config (#77). When present, hooks check each gate individually via `gate_enabled()`. Missing gate entries default to enabled, preserving default behavior.
1197
-
1198
- **What the binding means to a reader.** Three states, and the disposition differs for each. **Absent** = legacy = ENFORCE: a manifest written before #1123 (or by a stale skill body) binds nobody, so it must keep constraining everyone exactly as it did before — this is the only state that preserves the pre-#1123 contract, and `validate-wave-scope.mjs` marks it with one advisory stderr line rather than an error, because § 3.3's pre-union skeleton is itself an unbound manifest. **Own session** = ENFORCE, unchanged. **Foreign session** — `session` present and not this session's id — = ALLOW: `hooks/enforce-scope.mjs` lets the write through and emits `orchestrator.scope.foreign_session_ignored` so the skip is counted rather than silent. A foreign manifest is somebody else's wave plan; it never had authority over this session's writes, and the event is what keeps that visible instead of leaving an allow nothing recorded. (The reader half lives in `hooks/enforce-scope.mjs` — the writer's only obligation is to name itself honestly here.)
1199
- 2. Validate by piping through `node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs"` (where `$PLUGIN_ROOT` is `$CLAUDE_PLUGIN_ROOT`, `$CODEX_PLUGIN_ROOT`, or `$CURSOR_RULES_DIR` per platform — see `skills/_shared/config-reading.md`). If validation fails (exit 1), fix the JSON based on stderr errors and retry.
1200
- 3. **`allowedPaths` is COMPUTED from one canonical declaration array — never hand-transcribed (#1020/#1083).** Transcribing either declaration shape or the union by hand produced scope divergences. Globs stay verbatim (`scripts/*.sh`) — the enforcement hook resolves them at check time.
1201
-
1202
- **3.1 — materialize both declaration shapes once.** Build one JSON array from the session plan, one `{id, files}` record for every agent plus exactly one `coordinator` record for the coordinator's planned direct edits. `files` arrays, their entries and their order are the plan's verbatim declarations. Materialize it ONCE and capture the aggregate-sidecar path:
1203
-
1204
- ```bash
1205
- WAVE_SCOPE_RECORDS='[{"id":"W3-I1","files":["scripts/example.mjs"]},{"id":"coordinator","files":["skills/wave-executor/wave-loop.md"]}]'
1206
- WAVE_SCOPES_SIDECAR="$(
1207
- printf '%s' "$WAVE_SCOPE_RECORDS" | node "$PLUGIN_ROOT/scripts/materialize-wave-scope.mjs" \
1208
- --state-dir "$STATE_DIR" --wave "$WAVE"
1209
- )"
1210
- [ -n "$WAVE_SCOPES_SIDECAR" ] || { echo "materialize-wave-scope produced no sidecar path" >&2; exit 1; }
1211
- ```
1212
-
1213
- The non-empty check is not decoration. The materializer sends every diagnostic
1214
- to stderr, so a failure leaves `$WAVE_SCOPES_SIDECAR` empty, and an empty path
1215
- is what step 3.2 would then pass to `--assert-disjoint`. That combination used
1216
- to exit 0 with the collision gate never run — the same signal-free-ALLOW shape
1217
- #1083 exists to close. `validate-wave-scope.mjs` now refuses an empty flag
1218
- value as well, so this guard and that refusal are belt and braces.
1219
-
1220
- `materialize-wave-scope.mjs` validates the COMPLETE input before writing; it writes `<state-dir>/filescopes/wave-<N>/<agent-id>.json` as each bare `files` array first, then writes `<state-dir>/filescopes/wave-<N>.scopes.json` as the unchanged aggregate record array last. Its human stdout is only that final sidecar path, so the command substitution above is the canonical `$WAVE_SCOPES_SIDECAR`. On error, do not continue with a partial declaration set; correct the plan and run the one command again.
1221
-
1222
- The per-agent path IS `$AGENT_FILESCOPE_JSON` — the same file `--assert-subset` (#796 below), Grounding Injection (#85), the Learnings-Index (#1014) and File-Scope Injection (#1020) consume. Never write a `$TMPDIR` copy: it degrades to a signal-free allow when an injector cannot find the addressable wave-keyed file. The coordinator's record is materialized as `coordinator.json` and included in the aggregate, so its direct edits are covered by the two checks below.
1223
-
1224
- > **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs before the union exists, so writing these files reports `bash-write-verify: N file(s) changed by a Bash call OUTSIDE the wave's allowedPaths` naming `filescopes/wave-<N>/*.json`. Expected once per wave rollover at this step; it is information, not a scope violation. Never widen `allowedPaths` to silence it — that would grant agents write access to the deconfliction record itself.
1225
-
1226
- **3.2 — assert disjointness BEFORE computing the union.** The materialized aggregate is an ARRAY of `{id, files}` records (never an object map: a duplicated agent id must stay visible), including `coordinator.json`. Run:
1227
-
1228
- ```bash
1229
- node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
1230
- --assert-disjoint "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
1231
- ```
1232
-
1233
- Exit 1 (one stderr message per collision) means two agents were handed the same file: fix the session plan, re-materialize, re-assert. Never widen the union to make it pass. This runs **before** 3.3 because a union computed over colliding scopes launders the defect into the very artefact meant to prevent it — `allowedPaths` then grants the file and every later gate sees a legal write.
1234
-
1235
- **3.3 — compute the union.** `--union` is a QUERY MODE that still requires a schema-valid manifest on stdin, so write the skeleton first with `"allowedPaths": []`, then:
1236
-
1237
- ```bash
1238
- node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
1239
- --union "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
1240
- ```
1241
-
1242
- It prints the computed `allowedPaths` array as JSON on stdout **instead of** the manifest echo — one JSON document per run, the flag decides which. Insert that array as `allowedPaths`, then write the final `wave-scope.json`. It already applies the Test-Sibling Expansion below (`expandTestSiblings(unionFileScopes(scopes), { role })`, role read from the manifest), so do not also run the helper by hand.
1243
-
1244
- **Artifact production, disjointness and union computation are mechanized. Native prompt injection is a separate follow-up.** The materializer creates the durable declarations; the validator proves disjointness and computes the union. It does not install or prove the platform's prompt-injection transport, which remains independently responsible for reading `$AGENT_FILESCOPE_JSON` before dispatch.
1245
-
1246
- **The `--assert-subset` assertion (#796, below) stays unchanged and keeps running.** It checks a DIFFERENT property — each agent's scope ⊆ the union — and a double assignment is structurally invisible to it: a file claimed twice is a subset twice over. `--assert-disjoint` is an addition, never a replacement.
1247
-
1248
- **Test-Sibling Expansion (#970):** an `allowedPaths` entry that names a production file but NOT its test sibling makes the wave's own regression test unwritable — the scope guard then mechanically enforces exactly the inconsistency the quality gate exists to catch. Cross-repo evidence, three occurrences in ONE session: a migrations glob without the SQL-test directory (the regression test could not be written); a lone `.actions.ts` file (the wave's cross-tenant security test stayed red); a dead-export deletion whose importing test lay outside every scope (the suite ended red). Do NOT hand-derive the sibling paths — step 3.3's `--union` runs `expandTestSiblings(…, { role })` for you, so the hook, the validator and this prose state one rule.
1249
-
1250
- The helper is pure (same input → same output, no filesystem writes) and is also surfaced by `scripts/validate-wave-scope.mjs`. **The role decides, inside the helper** — `scripts/lib/scope-gate.mjs` `TEST_SIBLING_EXPANSION_ROLES` is THE list (currently `Impl-Core`, `Impl-Polish` — exactly where the incident occurred), and #5/#6 below describe that gate rather than restating it. Pass the role string; do not pre-filter by role in prose, and do not hand-roll the equivalent `{ enabled: … }`. Matching is trimmed + case-insensitive, so `impl-core` behaves as `Impl-Core`.
1251
-
1252
- > **Fail-closed:** an ABSENT or unrecognised `role` does **not** expand. Omitting it fails loudly (an agent's write to its own test is blocked, recoverable by one re-union); the opposite default would silently hand a Quality phase-1 simplification agent write access to the suite. `{ enabled: false }` is the unconditional opt-out and `{ enabled: true }` the explicit opt-in — both override the role.
1253
-
1254
- **It emits a GLOB, never a computed concrete path.** Resolve via the production file's basename, e.g. `foo.mjs` → `tests/**/foo*.test.mjs` — the same form `§ 4. Test-consolidation branch` already uses, stated once. Measured over all **439** tracked production `.mjs` in THIS repo (production = `scripts/**` + `hooks/**` + `skills/**`; tests = a top-level `tests/**` mirror with the `scripts/` prefix dropped): a same-basename test exists somewhere under `tests/` for **375/439 (85.4%)**, whereas a naive 1:1 mirror path resolves for only **272/439 (62.0%)**. So the glob is right ~85% of the time and *harmless* when wrong — it grants write access to a path that may not exist; a computed concrete path would be wrong ~38% of the time **and still deny the real test**. The ~15% residual is real, mostly semantic naming (`scripts/lib/learnings/*.mjs` → `tests/unit/learnings.test.mjs`): when an agent's test sibling does not match the glob, add it by hand to that agent's "Files:" scope in the session plan. This is an 85% default, not a guarantee.
1255
-
1256
- > Measured at `HEAD=730ee9d`, 2026-08-03, clean-tree, via `git ls-files | grep -E '^(scripts|hooks|skills)/.*\.mjs$'` for the denominator, matched against `git ls-files | grep -E '^tests/.*\.test\.mjs$'` by basename (85.4% figure) and by mirrored path (62.0% figure). Re-measure before citing these downstream — a count re-briefed later is a claim about the past (`.claude/rules/parallel-sessions.md` § PSA-006).
1257
-
1258
- **The sibling rule is repo-configurable, not a hardcoded layout.** THIS repo has zero `__tests__/` directories and no co-located tests; consumer-repo shapes (`<file>.test.*` beside the source, `<dir>/__tests__/**`, `supabase/migrations/** → supabase/tests/**`) are configured per repo and do not apply here.
1259
-
1260
- Three ordering constraints, all load-bearing:
1261
- - The deconfliction check (3.2) runs on the DECLARED per-agent scopes, **before** the union expands anything. Named ceiling: two agents whose production files share a basename receive the same emitted sibling glob, which a declared-scope check cannot see — revisit if a wave is ever scoped by basename family instead of by directory.
1262
- - Expand **before** `wave-scope.json` is written, in ONE pass. `hooks/post-bash-write-verify.mjs` fingerprints `allowedPaths` via `scopeSignature()` and fires a control notice on change, so a later mutation reads as tampering.
1263
- - Skip **absolute** entries entirely — expanding a Gate-5b out-of-repo grant would sprout a synthetic `tests/**` sibling outside the repo.
1264
-
1265
- **Pre-Dispatch Scope-Union Assertion (#796):** `wave-scope.json` is GLOBAL per wave — `hooks/enforce-scope.mjs` Gate 7 checks EVERY agent against the same `allowedPaths` union, so a union that (re)written for only ONE agent silently denies its siblings' legitimate writes. Before each `Agent()` batch, mechanically assert — for EVERY agent in the batch — that its fileScope ⊆ `wave-scope.allowedPaths`. `$AGENT_FILESCOPE_JSON` is that agent's § 3.1 file — `<state-dir>/filescopes/wave-<N>/<agent-id>.json`, already written above and shared with every other consumer. Do not re-write it to a temp path here (§ 3.1 says why that degrades silently); just run:
1266
-
1267
- ```bash
1268
- node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
1269
- --assert-subset "$AGENT_FILESCOPE_JSON" --expand-test-siblings \
1270
- < <state-dir>/wave-scope.json
1271
- ```
1272
-
1273
- `--expand-test-siblings` (#970) is the mechanical half of the Test-Sibling Expansion rule above: it re-derives each agent's siblings and requires the union to grant them, so "the coordinator ran the expansion" stops being a matter of prose compliance. Pass it on **every** batch — the flag is gated on the manifest's own `role` through the same `TEST_SIBLING_EXPANSION_ROLES` predicate the helper uses, so it is a self-announcing no-op (`WARN: … skipped for role "Quality"`) wherever expansion does not fire. Do not add a role condition in the shell; that would put the role list back in prose.
1274
-
1275
- It only ever ADDS a requirement, so a manifest that passed the plain subset check can now fail — that is the point. On exit 1 (`allowedPaths does not grant the test sibling … missing: [...]`): the union was not produced by `expandTestSiblings`. Re-run the Scope Manifest step, rewrite `wave-scope.json`, re-assert. Do NOT hand-add the missing glob and move on — the next agent in the batch will hit the same gap. (If a legitimate test sibling does not match the emitted glob — the ~15% residual — it belongs in that agent's "Files:" scope in the session plan, which puts it in the union and satisfies the check honestly.)
1276
-
1277
- On exit 1 (`agent fileScope not ⊆ allowedPaths — missing: [...]`): re-union `allowedPaths` across ALL agents that will be in-flight — **including still-running siblings from this wave** — re-write `wave-scope.json`, then re-run the assertion before dispatching. `allowedPaths` MUST NEVER shrink while sibling agents of the same wave are still running. This applies to EVERY batch — including fix-pass and re-dispatch batches, the incident class that motivated #796 (a fix-pass batch rewrote the union for a single agent and denied a sibling's legitimate writes). The assertion runs uniformly, even for single-agent waves — cost is negligible and the invariant is the same.
1278
- 4. Read `enforcement` from Session Config (default: `warn`). The `enforcement` field is REQUIRED in `wave-scope.json` — always write it explicitly. The hooks default to `warn` if the field is missing, which would silently degrade strict enforcement. If jq was confirmed missing in Pre-Execution Check step 4, set `enforcement` to `off` and include a comment in the progress update noting that enforcement is disabled.
1279
- 5. For **Discovery** role waves, set `allowedPaths` to `[]` (empty array) — Discovery agents are read-only and must not modify files. Also add to each Discovery agent prompt: "You are READ-ONLY. Do NOT use Edit or Write tools."
1280
- > **Defense in depth:** The empty `allowedPaths` enforcement hook is the PRIMARY barrier (blocks Write/Edit at the tool level). The prompt instruction is a SECONDARY safeguard. If jq is unavailable (enforcement set to `off`), the prompt instruction becomes the ONLY barrier — log a warning in this case.
1281
- > **Test-sibling expansion (#970) cannot reach here, twice over:** `Discovery` is not in `TEST_SIBLING_EXPANSION_ROLES`, and `expandTestSiblings([], …)` returns `[]` STRUCTURALLY — before any gate, so the empty case holds even for a caller that opts in explicitly. Discovery's deny-all is a contract with its own regression test (`tests/hooks/enforce-scope.test.mjs`, "enforces Discovery-wave deny-all semantics… (issue #256 NO-OP contract)") — nothing may re-populate it.
1282
- 6. For **Quality** role waves, use two-phase scope enforcement:
1283
- - **Phase 1 (Simplification)**: Before dispatching simplification agents, set `allowedPaths` to the production files changed this session (`git diff --name-only $SESSION_START_REF..HEAD`, excluding test files). After simplification agents complete, **delete** `<state-dir>/wave-scope.json` before proceeding to Phase 2.
1284
- Test-sibling expansion (#970) does not fire here, because `Quality` is not in `TEST_SIBLING_EXPANSION_ROLES` — passing `{ role: 'Quality' }` is enough and no extra opt-out is needed. This phase's scope is production files with tests explicitly excluded, under a "do NOT change functionality" brief — expanding would hand simplification agents write access to the suite, which is the "agent deletes a dead export, then edits the test to match" failure mode. The pre-dispatch `--expand-test-siblings` assertion is gated on the same role and self-announces its skip, so it cannot block this phase either.
1285
- - **Phase 2 (Test/Review)**: Before dispatching test and review agents, regenerate `<state-dir>/wave-scope.json` with `allowedPaths` restricted to test file patterns (`**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`, plus test config files). Quality test/review agents must not modify production source code.
1286
- Test-sibling expansion (#970) is **inert** here — the scope is already test patterns. It must never add production paths; the inverse expansion (test → production sibling) does not exist and must not be introduced.
1287
-
1288
- **Phase transition sequence:**
1289
- 1. Compute production file list: `git diff --name-only $SESSION_START_REF..HEAD | grep -v -E '\.(test|spec)\.' | grep -v '__tests__/'`
1290
- 2. If no production files → skip Phase 1 entirely, proceed to Phase 2 (write test-only wave-scope.json)
1291
- 3. Write Phase 1 wave-scope.json with production file allowedPaths
1292
- 4. Dispatch simplification agents, wait for completion
1293
- 5. Delete `<state-dir>/wave-scope.json`
1294
- 6. Write Phase 2 wave-scope.json with test file allowedPaths (`**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`)
1295
- 7. Dispatch test/review agents
1296
- 7. After the final wave completes, delete `<state-dir>/wave-scope.json` (cleanup). Delete `<state-dir>/filescopes/` in the same step — the per-agent scope files (§ 3.1) are wave-local working state, and a stale `wave-<N>/` directory left behind is a scope claim nobody re-verified.
35
+ **Shell variables used in this section:**
36
+ - `$PLUGIN_ROOT` — harness-supplied; `$CLAUDE_PLUGIN_ROOT`, `$CODEX_PLUGIN_ROOT`, or `$CURSOR_RULES_DIR` per platform — see `skills/_shared/config-reading.md`.
37
+ - `$WAVE` — the current wave number (the `<N>` in `<state-dir>/filescopes/wave-<N>/`).
38
+ - `$ROLE` — the wave's assigned role, resolved from the session plan's role-to-wave mapping (§ 0 above).
39
+ - `$STATE_DIR` — same value as the `<state-dir>` placeholder used elsewhere in this doc: `.claude` / `.codex` / `.cursor` per platform.