session-orchestrator 3.17.0 → 3.20.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 (221) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/030-wave-execution.mdc +17 -1
  5. package/CHANGELOG.md +185 -412
  6. package/README.md +12 -9
  7. package/SECURITY.md +190 -27
  8. package/agents/AGENTS.md +20 -3
  9. package/agents/code-implementer.md +6 -6
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/qa-strategist.md +31 -6
  12. package/agents/schemas/qa-strategist.schema.json +27 -0
  13. package/agents/schemas/test-writer.schema.json +60 -2
  14. package/agents/security-reviewer.md +1 -1
  15. package/agents/session-reviewer.md +1 -1
  16. package/agents/test-writer.md +29 -10
  17. package/agents/ui-developer.md +1 -1
  18. package/commands/contract-version-bump.md +28 -0
  19. package/commands/portfolio.md +1 -1
  20. package/commands/session.md +6 -2
  21. package/docs/USER-GUIDE.md +9 -4
  22. package/docs/ci-setup.md +121 -7
  23. package/docs/codex-setup.md +1 -1
  24. package/docs/components.md +6 -6
  25. package/docs/cursor-setup.md +22 -9
  26. package/docs/events-schema.md +5 -1
  27. package/docs/instruction-delivery.md +794 -0
  28. package/docs/rule-authoring.md +58 -9
  29. package/docs/session-config-reference.md +245 -50
  30. package/docs/session-config-template.md +39 -26
  31. package/hooks/_lib/guard-source-loader.mjs +680 -0
  32. package/hooks/_lib/lock-bootstrap.mjs +21 -0
  33. package/hooks/_lib/vcs-create-matcher.mjs +119 -0
  34. package/hooks/config-protection.mjs +0 -0
  35. package/hooks/enforce-commands.mjs +226 -19
  36. package/hooks/enforce-scope.mjs +133 -9
  37. package/hooks/hooks-codex.json +1 -1
  38. package/hooks/hooks-cursor.json +11 -2
  39. package/hooks/hooks-pi.json +10 -0
  40. package/hooks/hooks.json +21 -1
  41. package/hooks/on-session-end.mjs +178 -18
  42. package/hooks/on-session-start.mjs +30 -4
  43. package/hooks/post-bash-write-verify.mjs +977 -0
  44. package/hooks/post-subagent-discovery-validator.mjs +256 -41
  45. package/hooks/pre-bash-destructive-guard.mjs +616 -164
  46. package/hooks/pre-bash-issue-budget.mjs +167 -0
  47. package/hooks/pre-bash-sessions-ledger-guard.mjs +1054 -0
  48. package/hooks/pre-bash-templates-first.mjs +96 -63
  49. package/hooks/subagent-telemetry.mjs +527 -37
  50. package/package.json +6 -3
  51. package/pi/prompts/contract-version-bump.md +12 -0
  52. package/rules/README.md +32 -0
  53. package/scripts/archive-closed-prds.mjs +12 -22
  54. package/scripts/autopilot-multi.mjs +103 -20
  55. package/scripts/backfill-abandoned-sessions.mjs +160 -4
  56. package/scripts/backfill-learnings-from-vault.mjs +967 -0
  57. package/scripts/check-doc-consistency.sh +17 -1
  58. package/scripts/emit-session.mjs +3 -40
  59. package/scripts/eval-session.mjs +50 -9
  60. package/scripts/fleet-instruction-scan.mjs +141 -0
  61. package/scripts/lib/autopilot/mr-draft.mjs +31 -1
  62. package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
  63. package/scripts/lib/backlog-scan.mjs +39 -6
  64. package/scripts/lib/blocked-commands-policy.mjs +340 -0
  65. package/scripts/lib/ci-status-banner.mjs +75 -12
  66. package/scripts/lib/claude-md-budget-lint.mjs +283 -34
  67. package/scripts/lib/command-blocker.mjs +1273 -58
  68. package/scripts/lib/config/config-protection.mjs +2 -1
  69. package/scripts/lib/config/drift-check.mjs +9 -1
  70. package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
  71. package/scripts/lib/config/issue-budget.mjs +123 -0
  72. package/scripts/lib/config/reconcile.mjs +21 -0
  73. package/scripts/lib/config/section-extractor.mjs +121 -1
  74. package/scripts/lib/config-schema.mjs +23 -3
  75. package/scripts/lib/config.mjs +17 -0
  76. package/scripts/lib/convergence-monitor.mjs +49 -3
  77. package/scripts/lib/description-surface.mjs +535 -0
  78. package/scripts/lib/dispatcher/enumerate.mjs +26 -40
  79. package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
  80. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
  81. package/scripts/lib/eval/engine.mjs +47 -5
  82. package/scripts/lib/events.mjs +59 -7
  83. package/scripts/lib/gates/gate-full.mjs +15 -3
  84. package/scripts/lib/gates/gate-helpers.mjs +132 -6
  85. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
  86. package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
  87. package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
  88. package/scripts/lib/handover-gate.mjs +7 -3
  89. package/scripts/lib/hardening.mjs +9 -9
  90. package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
  91. package/scripts/lib/instruction-budget-guard.mjs +402 -51
  92. package/scripts/lib/io.mjs +345 -10
  93. package/scripts/lib/issue-budget.mjs +269 -0
  94. package/scripts/lib/issue-close-strip-labels.mjs +39 -9
  95. package/scripts/lib/label-scope.mjs +47 -0
  96. package/scripts/lib/learnings/affinity.mjs +434 -0
  97. package/scripts/lib/learnings/candidates.mjs +736 -0
  98. package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
  99. package/scripts/lib/learnings/judgment.mjs +782 -0
  100. package/scripts/lib/learnings/kebab.mjs +128 -0
  101. package/scripts/lib/learnings/schema.mjs +43 -3
  102. package/scripts/lib/learnings/select.mjs +550 -0
  103. package/scripts/lib/lock-reaper.mjs +1 -2
  104. package/scripts/lib/memory-proposals/schema.mjs +36 -1
  105. package/scripts/lib/peer-discovery.mjs +645 -0
  106. package/scripts/lib/pi-hook-bridge.mjs +146 -17
  107. package/scripts/lib/product-repo-detect.mjs +9 -8
  108. package/scripts/lib/project-hygiene.mjs +432 -0
  109. package/scripts/lib/quality-gate.mjs +167 -0
  110. package/scripts/lib/recommendations-v0.mjs +1 -1
  111. package/scripts/lib/reconcile/eligibility.mjs +1 -1
  112. package/scripts/lib/reconcile/emitter.mjs +128 -24
  113. package/scripts/lib/reconcile/engine.mjs +156 -54
  114. package/scripts/lib/reconcile/idempotency.mjs +114 -14
  115. package/scripts/lib/reconcile/renderer.mjs +141 -25
  116. package/scripts/lib/reconcile/sanitize.mjs +518 -0
  117. package/scripts/lib/reconcile/writer.mjs +95 -1
  118. package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
  119. package/scripts/lib/resource-probe/evaluate.mjs +70 -4
  120. package/scripts/lib/resource-probe.mjs +19 -0
  121. package/scripts/lib/rule-loader.mjs +6 -0
  122. package/scripts/lib/scope-baseline.mjs +564 -0
  123. package/scripts/lib/scope-gate.mjs +568 -145
  124. package/scripts/lib/session-close-backfill.mjs +63 -8
  125. package/scripts/lib/session-end/phase-skip.mjs +1 -0
  126. package/scripts/lib/session-id.mjs +221 -41
  127. package/scripts/lib/session-lock.mjs +304 -6
  128. package/scripts/lib/session-record-repair.mjs +551 -0
  129. package/scripts/lib/session-schema/constants.mjs +22 -3
  130. package/scripts/lib/session-schema/serializer.mjs +54 -0
  131. package/scripts/lib/session-schema/validator.mjs +16 -0
  132. package/scripts/lib/session-schema.mjs +1 -0
  133. package/scripts/lib/session-token-rollup.mjs +68 -6
  134. package/scripts/lib/sessions-integrity-banner.mjs +294 -0
  135. package/scripts/lib/sessions-staleness-banner.mjs +121 -12
  136. package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
  137. package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
  138. package/scripts/lib/soul-resolve.mjs +12 -0
  139. package/scripts/lib/spiral-carryover.mjs +142 -30
  140. package/scripts/lib/state-md/mission-status.mjs +53 -3
  141. package/scripts/lib/subagents-schema.mjs +43 -9
  142. package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
  143. package/scripts/lib/tests-src-ratio.mjs +484 -0
  144. package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
  145. package/scripts/lib/validate/check-agents.mjs +56 -0
  146. package/scripts/lib/validate/check-banner-parity.mjs +376 -0
  147. package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
  148. package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
  149. package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
  150. package/scripts/lib/validate/check-owner-leakage.mjs +3 -3
  151. package/scripts/lib/validate/check-rules.mjs +244 -36
  152. package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
  153. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  154. package/scripts/lib/validate/check-unwired-features.mjs +549 -0
  155. package/scripts/lib/validate-vendored-rules.mjs +10 -2
  156. package/scripts/lib/vault-archive.mjs +17 -2
  157. package/scripts/lib/vault-backfill/glab.mjs +8 -0
  158. package/scripts/lib/vault-mirror/process.mjs +30 -0
  159. package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
  160. package/scripts/lib/vcs-repo-spec.mjs +362 -0
  161. package/scripts/lib/wave-resource-gate.mjs +115 -11
  162. package/scripts/lib/worktree/listing.mjs +44 -7
  163. package/scripts/mcp-server.sh +17 -3
  164. package/scripts/measure-context-overhead.sh +151 -0
  165. package/scripts/memory-propose.mjs +72 -9
  166. package/scripts/print-applicable-rules.mjs +218 -16
  167. package/scripts/print-learnings-index.mjs +474 -0
  168. package/scripts/release.mjs +534 -0
  169. package/scripts/repair-invalid-sessions.mjs +209 -0
  170. package/scripts/run-quality-gate.mjs +123 -5
  171. package/scripts/sweep-expired-learnings.mjs +192 -32
  172. package/scripts/validate-plugin.mjs +21 -0
  173. package/scripts/validate-wave-scope.mjs +182 -17
  174. package/scripts/vault-integration-watcher.mjs +32 -10
  175. package/skills/_shared/config-reading.md +2 -2
  176. package/skills/bootstrap/fast-template.md +1 -1
  177. package/skills/brainstorm/soul.md +47 -1
  178. package/skills/claude-md-drift-check/checker.mjs +145 -28
  179. package/skills/contract-version-bump/SKILL.md +219 -0
  180. package/skills/discovery/SKILL.md +4 -4
  181. package/skills/discovery/issue-templates.md +11 -11
  182. package/skills/discovery/probes-audit.md +1 -1
  183. package/skills/discovery/probes-feature.md +1 -1
  184. package/skills/discovery/probes-session.md +26 -5
  185. package/skills/ecosystem-health/SKILL.md +1 -1
  186. package/skills/ecosystem-health/wizard.md +4 -4
  187. package/skills/evolve/SKILL.md +117 -18
  188. package/skills/gitlab-ops/SKILL.md +25 -12
  189. package/skills/gitlab-portfolio/SKILL.md +2 -2
  190. package/skills/grill/soul.md +44 -1
  191. package/skills/hook-development/SKILL.md +1 -1
  192. package/skills/mode-selector/SKILL.md +1 -1
  193. package/skills/npm-publish/SKILL.md +17 -1
  194. package/skills/plan/SKILL.md +5 -5
  195. package/skills/plan/mode-feature.md +4 -4
  196. package/skills/plan/mode-new.md +10 -10
  197. package/skills/plan/mode-retro.md +1 -1
  198. package/skills/plan/soul.md +46 -3
  199. package/skills/quality-gates/SKILL.md +1 -1
  200. package/skills/reconcile/SKILL.md +21 -4
  201. package/skills/session-end/SKILL.md +34 -36
  202. package/skills/session-end/discovery-scan.md +4 -2
  203. package/skills/session-end/drift-operations.md +4 -4
  204. package/skills/session-end/metrics-collection.md +13 -0
  205. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  206. package/skills/session-end/phase-3-6-tail.md +32 -2
  207. package/skills/session-end/plan-verification.md +6 -7
  208. package/skills/session-end/session-metrics-write.md +2 -0
  209. package/skills/session-end/vault-operations.md +1 -1
  210. package/skills/session-end/verification-checklist.md +1 -1
  211. package/skills/session-plan/SKILL.md +6 -2
  212. package/skills/session-plan/wave-template.md +2 -0
  213. package/skills/session-start/SKILL.md +75 -7
  214. package/skills/session-start/phase-4-5-resource-health.md +15 -2
  215. package/skills/session-start/soul.md +41 -1
  216. package/skills/test-runner/SKILL.md +2 -2
  217. package/skills/vault-sync/validator.mjs +108 -7
  218. package/skills/wave-executor/SKILL.md +6 -7
  219. package/skills/wave-executor/circuit-breaker.md +2 -0
  220. package/skills/wave-executor/wave-loop.md +198 -80
  221. package/templates/_shared/loop.md +4 -4
@@ -29,6 +29,34 @@ For each wave, resolve its assigned role(s) from the session plan's role-to-wave
29
29
  3. Proceed to next wave immediately
30
30
  4. Do NOT write wave-scope.json for skipped waves
31
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
+
32
60
  ### 0.5. Pre-Dispatch Resource Gate (#193)
33
61
 
34
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).
@@ -179,6 +207,49 @@ Before dispatching each agent whose fileScope contains a NEW (non-existent) file
179
207
 
180
208
  **If 0 candidates:** dispatch unchanged — same silent-no-op convention as Grounding Injection / Frontmatter-Guard above. Never blocks dispatch.
181
209
 
210
+ #### Pre-Dispatch: Fact-Staleness Annotation (#908)
211
+
212
+ 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.
213
+
214
+ **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.
215
+
216
+ **Trigger — annotate when ANY of these holds (no judgement call):**
217
+
218
+ 1. `now − measured_at ≥ 5 min`
219
+ 2. `measured_at` is absent
220
+ 3. the peer probe below reported `live: true` for the repo the fact is about
221
+
222
+ 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).
223
+
224
+ **Peer signal — once per wave, plus once per distinct foreign repo cited:**
225
+
226
+ ```bash
227
+ node "$PLUGIN_ROOT/scripts/lib/peer-discovery.mjs" --check-live "<repoRoot>" --json
228
+ # → {"live":false,"reason":"no-lock","probe":"lock-only","peerCount":0,"peer":null}
229
+ ```
230
+
231
+ 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".
232
+
233
+ 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`.
234
+
235
+ **Annotation format** — in the agent prompt, replace the bare value with:
236
+
237
+ ASSERTED (age <N> min, source W<k>/<agent>): <value>. Verify command: <cmd>. Run it before relying on this.
238
+
239
+ **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:
240
+
241
+ UNVERIFIED (no measurement command, source W<k>/<agent>): <claim>. Establish it yourself before relying on this.
242
+
243
+ **Worked examples:**
244
+
245
+ | Fact | Decision |
246
+ |---|---|
247
+ | "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.` |
248
+ | "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 |
249
+ | 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 |
250
+
251
+ **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.
252
+
182
253
  #### Agent-Type Resolution
183
254
 
184
255
  Each agent in the session plan specifies a `subagent_type`. Use that value directly when dispatching:
@@ -192,6 +263,8 @@ For each agent in this wave:
192
263
  - Which files to read/modify (exact paths)
193
264
  - Acceptance criteria (how to verify done)
194
265
  - Relevant patterns — injected automatically as the <APPLICABLE-RULES> block (see Pre-Dispatch: Glob-Scoped Rule Injection below)
266
+ - Relevant past learnings — injected automatically as the <LEARNINGS-INDEX> block, computed PER AGENT from its file scope (see Pre-Dispatch: Learnings-Index Injection below)
267
+ - 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)
195
268
  - VCS issue reference if applicable
196
269
  - What NOT to touch (other agents' files)
197
270
  >",
@@ -322,6 +395,8 @@ Behaviour change: agents writing vault notes now receive the canonical schema en
322
395
 
323
396
  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`.
324
397
 
398
+ > **⚠ 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.
399
+
325
400
  **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).
326
401
 
327
402
  **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.
@@ -331,7 +406,7 @@ After `wave-scope.json` is written for this wave and before assembling the `Agen
331
406
  RULES_BLOCK="$(node "$PLUGIN_ROOT/scripts/print-applicable-rules.mjs" --context wave 2>/dev/null)"
332
407
 
333
408
  `--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:
334
- - a Markdown block (header `## Applicable Rules (scoped to this wave)` + each matching rule's raw content, separated by `---`) when one or more rules apply, OR
409
+ - 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
335
410
  - empty output (exit 0) when no rules match — in which case prepend nothing.
336
411
 
337
412
  **Prompt assembly:** when `$RULES_BLOCK` is non-empty, prepend it to EACH agent's prompt in this wave under a clear separator:
@@ -346,6 +421,40 @@ When `$RULES_BLOCK` is empty (no `.claude/rules/`, no matching rules, or any CLI
346
421
 
347
422
  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.
348
423
 
424
+ #### Pre-Dispatch: Learnings-Index Injection (#1014)
425
+
426
+ > **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.
427
+
428
+ 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.
429
+
430
+ **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.
431
+
432
+ **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.
433
+
434
+ **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".
435
+
436
+ **Zero new coordinator obligations.** The per-agent file scope this needs is the SAME `$AGENT_FILESCOPE_JSON` temp file the Pre-Dispatch Scope-Union Assertion (#796, see `## Scope Manifest` § 3) already requires you to write for every agent in every `Agent()` batch. Reuse that file — do not write a second one.
437
+
438
+ **Invocation:** once per agent, immediately after that agent's `$AGENT_FILESCOPE_JSON` is written, capture stdout as `$LEARNINGS_INDEX`:
439
+
440
+ LEARNINGS_INDEX="$(node "$PLUGIN_ROOT/scripts/print-learnings-index.mjs" \
441
+ --file-scope "$AGENT_FILESCOPE_JSON" \
442
+ --task-text "<the agent's task title / one-line description>" 2>/dev/null)"
443
+
444
+ `--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.
445
+
446
+ **Prompt assembly:** when `$LEARNINGS_INDEX` is non-empty, prepend it to THAT agent's prompt:
447
+
448
+ <LEARNINGS-INDEX>
449
+ $LEARNINGS_INDEX
450
+ </LEARNINGS-INDEX>
451
+
452
+ <original prompt>
453
+
454
+ 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.
455
+
456
+ **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.
457
+
349
458
  #### Structured Reasoning (STATE:/PLAN:) — opt-in via `reasoning-output: true` (#79)
350
459
 
351
460
  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.
@@ -552,7 +661,7 @@ Log every non-`pass` result as an event to `.orchestrator/metrics/events.jsonl`
552
661
  - After **Impl-Polish**: Incremental quality checks + integration verification
553
662
  - **Simplification pass** (at the start of the Quality wave, before test/review agents):
554
663
  1. Identify all files changed in this session: `git diff --name-only $SESSION_START_REF..HEAD`
555
- 2. Filter to production files only (exclude `*.test.*`, `*.spec.*`, `__tests__/`). If no production files changed, skip the simplification pass entirely — proceed directly to test/review agents.
664
+ 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.
556
665
  3. Dispatch 1-2 simplification agents with:
557
666
  - Changed file list (production files only — exclude `*.test.*`, `*.spec.*`, `__tests__/`)
558
667
  - Reference: `slop-patterns.md` from the discovery skill directory — include the actual patterns in the agent prompt
@@ -561,7 +670,16 @@ Log every non-`pass` result as an event to `.orchestrator/metrics/events.jsonl`
561
670
  - 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."
562
671
  - Tools: Read, Edit, Grep, Glob
563
672
  - Model: sonnet
564
- 4. After simplification agents complete, proceed to Quality test/review agents
673
+ 4. **Test-consolidation branch** in the SAME dispatch round as step 3, dispatch exactly 1 test-consolidation agent with:
674
+ - 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`)
675
+ - 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."
676
+ - **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.
677
+ - **Report**: the agent MUST emit `test_delta: {added, removed, consolidated, net_loc}` in its report so the coordinator can record the pass's effect.
678
+ - Tools: Read, Edit, Grep, Glob
679
+ - Model: sonnet
680
+ 5. After the simplification and test-consolidation agents complete, proceed to Quality test/review agents
681
+ - **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.
682
+ 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.
565
683
  - After **Quality**: Full Gate quality checks per quality-gates (typecheck + test + lint, must all pass)
566
684
  (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.)
567
685
  - After **Finalization**: final git status check
@@ -584,29 +702,6 @@ Per attempt:
584
702
  See `SKILL.md` § "Inter-Wave Quality-Gate (with Auto-Fix Loop — #521)" for
585
703
  the full invocation pattern.
586
704
 
587
- ##### /goal Continuation Anchor (opt-in — #636)
588
-
589
- > Advisory-only continuation anchor at the inter-wave fix-loop seam. Never auto-invokes `/goal`, never blocks forward progress. `/goal` is a user slash-command; the operator decides whether to use it.
590
-
591
- **Gate conditions** — ALL must be true for this nudge to surface:
592
-
593
- 1. `goal-integration.enabled: true` in Session Config (default: `false`).
594
- 2. `inter-wave-fixloop` is listed in `goal-integration.seams`.
595
-
596
- When any gate condition is false, skip this step entirely — proceed to `##### STATE.md Deviation — Auto-Fix Result`.
597
-
598
- **What it does** — when the gate fires and the inter-wave Quality-Gate is failing (auto-fix retries in flight or about to begin), surface ONE suggested `/goal` command as an advisory bullet in the wave progress update. Example:
599
-
600
- ```
601
- /goal Keep fixing Wave <N> quality-gate failures until 'npm run lint', 'npm run typecheck' and 'npm test' each print 0 failures in this turn's output, or stop after <max-retries+1> attempts.
602
- ```
603
-
604
- **Advisory-only contract:** the `/goal` is the continuation anchor that keeps the coordinator working across turns while it iterates on the fix. The exit-code result of `runQualityGateWithRetry()` stays the judgment — `/goal` continues the loop, it never decides correctness. The hard-abort + diagnostics-bundle path (`.orchestrator/metrics/verification-failures/<ts>.json` after `max-retries`) is UNCHANGED: an active `/goal` does not extend, replace, or bypass the bounded retry ceiling. This step is informational prose only — no AskUserQuestion, no STATE.md write, no sidecar.
605
-
606
- The `/goal` evaluator reads the transcript only and runs NO tools — it anchors CONTINUATION, never JUDGMENT. The suggested condition therefore references freshly-run gate output "in this turn's output" and embeds a bound ("or stop after N attempts"). Cross-reference `.claude/rules/loop-and-monitor.md § LM-008` for the full `/goal` continuation-vs-judgment contract rather than restating it here.
607
-
608
- **One goal per session:** only ONE `/goal` can be active at a time. This inter-wave fix-loop seam and the session-end backlog seam (`skills/session-end/SKILL.md` § 1.3a) cannot both hold an active goal simultaneously — the operator picks one.
609
-
610
705
  ##### STATE.md Deviation — Auto-Fix Result
611
706
 
612
707
  After `runQualityGateWithRetry()` returns:
@@ -732,8 +827,37 @@ If the commit itself fails (e.g., nothing to commit, pre-commit hook rejects), d
732
827
  - `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.
733
828
  - `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`.
734
829
  - `quality_check`: incremental check result (pass/fail/skipped)
830
+ - `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.
831
+ **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:
832
+
833
+ ```bash
834
+ jq -c --argjson w <wave_number> --arg s "<semantic_session_id>" '
835
+ select(.event | startswith("orchestrator.quality_gate."))
836
+ | select(.semantic_session_id == $s and .wave_number == $w and .counts != null)
837
+ | .counts' .orchestrator/metrics/events.jsonl | tail -1
838
+ ```
839
+
840
+ The session filter is not optional — `events.jsonl` accumulates across sessions and every past session also had a wave with this number.
841
+ **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.
842
+ > **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.
735
843
  Append this wave record to the session metrics `waves` array.
736
844
 
845
+ 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.
846
+
847
+ ```js
848
+ import { computeDrift } from '$PLUGIN_ROOT/scripts/lib/scope-baseline.mjs';
849
+
850
+ const drift = computeDrift({ repoRoot: process.cwd(), threshold: 2.0 });
851
+ if (drift.skipped === false && drift.breached) {
852
+ console.warn(
853
+ `⚠ Scope drift: filesRatio ${drift.filesRatio} (${drift.actualFiles} actual / ${drift.plannedFiles} planned files) ` +
854
+ `>= threshold ${drift.threshold} — session has grown beyond its frozen scope baseline.`
855
+ );
856
+ }
857
+ ```
858
+
859
+ 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.
860
+
737
861
  ### 3. Adapt Plan (if needed)
738
862
 
739
863
  After reviewing wave results, decide:
@@ -774,8 +898,12 @@ After each wave completes and before the progress update, update `<state-dir>/ST
774
898
  1. **Frontmatter**: set `current-wave` to the just-completed wave number; set `status` to `active` (or `paused` if waiting on user input)
775
899
  2. **`## Current Wave`**: replace contents with next wave info — wave number, role, agents to dispatch and count
776
900
  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):
901
+ > **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.
902
+ >
903
+ > **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.
904
+
777
905
  ```
778
- ### Wave N — <Role> (planned <P> files → actual <A>, over-delivery <R>)
906
+ ### Wave N — <Role> (planned <P> files → actual <A>, over-delivery <R>) — suite <passed>/<failed> on <platform>
779
907
  - Agent "<description>": <done|partial|failed> — <files changed> — <1-line note>
780
908
  - Agent "<description>": <done|partial|failed> — <files changed> — <1-line note>
781
909
  ```
@@ -921,53 +1049,6 @@ When the hook is skipped (gate condition false), omit the `persona_gate` field e
921
1049
 
922
1050
  **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.
923
1051
 
924
- ### 3c. Strategic Compact-Nudge (#620)
925
-
926
- > Advisory-only checkpoint. Never auto-compacts. `/compact` is a user slash-command; the coordinator/operator decides when to invoke it.
927
-
928
- **Gate conditions** — ALL must be true for the nudge to emit:
929
-
930
- 1. `compact-nudge.enabled: true` in Session Config (default: `false`).
931
- 2. The just-completed wave's role is listed in `compact-nudge.after` (default: `['discovery', 'impl']`). Compare the wave's canonical role string (lower-case) against the list.
932
- 3. `compact-nudge.mode !== 'off'` (when `mode: 'off'` the nudge is a silent no-op even when `enabled: true`).
933
-
934
- When any gate condition is false, skip this step entirely — proceed to `### 4. Progress Update`.
935
-
936
- **Nudge format** — when the gate fires, append ONE advisory bullet to the wave progress update (step `### 4`):
937
-
938
- ```
939
- - 💡 Compact checkpoint: Wave N (<Role>) complete — consider /compact before Wave N+1 (<NextRole>) to free context (advisory only; see decision table). Never auto-compacts.
940
- ```
941
-
942
- **What survives `/compact` vs what is lost:**
943
-
944
- | Survives | Lost |
945
- |---|---|
946
- | CLAUDE.md, STATE.md (on disk), wave-scope.json, JSONL metrics (.orchestrator/), git history, all files on disk | Intermediate reasoning/thinking traces, previously-read file contents cached in context, tool-call history for prior waves |
947
-
948
- This frames the nudge: the persistent artefacts (plan, scope, STATE.md, git diff) are the distilled output of completed work; losing in-context file reads is the cost. Compact is worth it when the completed wave produced bulky research/audit output that is unlikely to be re-referenced verbatim.
949
-
950
- **Decision table:**
951
-
952
- | Wave boundary (completed → next) | Compact? | Why |
953
- |---|---|---|
954
- | Discovery → Impl-Core | Yes | Research/audit context is bulky; the plan + wave-scope.json is the distilled output. |
955
- | Impl-Core → Impl-Polish (long Core) | Maybe | Compact only if Polish targets different files; keep if Polish builds on Core's changes. |
956
- | Impl-Polish → Quality | No | Quality references the just-written code; losing it is costly. |
957
- | Quality → Finalization | No | Finalization needs the full session diff. |
958
- | Mid-implementation (within a wave) | No | Losing file paths + partial state is expensive. |
959
- | After a FAILED/aborted wave | Yes | Clear the dead-end reasoning before the adapted retry. |
960
- | Switching to an unrelated task block (deep session) | Yes | Debug/exploration traces pollute unrelated downstream work. |
961
-
962
- **Behaviour by mode:**
963
-
964
- | `mode` | Action |
965
- |--------|--------|
966
- | `off` | No nudge (gate condition above). |
967
- | `warn` | Emit the advisory bullet in the wave progress update. Coordinator/operator acts at their discretion. |
968
-
969
- The nudge is informational only — no AskUserQuestion, no state-md write, no sidecar. This step never blocks forward progress.
970
-
971
1052
  ### 4. Progress Update
972
1053
 
973
1054
  After each wave, provide a brief status:
@@ -990,13 +1071,17 @@ Before each wave dispatch:
990
1071
  1. **Write `<state-dir>/wave-scope.json`** with the wave's scope:
991
1072
  > (Platform-specific: `.claude/wave-scope.json` on Claude Code, `.codex/wave-scope.json` on Codex CLI, `.cursor/wave-scope.json` on Cursor IDE)
992
1073
 
993
- **Deriving `blockedCommands` (policy-file-first, #155):** Before writing `wave-scope.json`, extract the blocked patterns from the consolidated policy file:
1074
+ **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.)
994
1075
  ```bash
995
- BLOCKED=$(jq -c '[.rules[] | select(.severity == "block") | .pattern]' .orchestrator/policy/blocked-commands.json)
1076
+ BLOCKED=$(node --input-type=module -e "
1077
+ import { loadEffectivePolicy } from '$PLUGIN_ROOT/scripts/lib/blocked-commands-policy.mjs';
1078
+ const { rules } = await loadEffectivePolicy({ cwd: process.cwd(), projectDir: process.env.CLAUDE_PROJECT_DIR ?? null, pluginRoot: '$PLUGIN_ROOT' });
1079
+ console.log(JSON.stringify((rules ?? []).filter(r => r.severity === 'block').map(r => r.pattern)));
1080
+ ")
996
1081
  ```
997
- Use `$BLOCKED` as the `blockedCommands` value in `wave-scope.json`.
1082
+ 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.
998
1083
 
999
- **Fallback:** If `.orchestrator/policy/blocked-commands.json` is missing (pre-#155 repo), use the legacy hardcoded array and log a warning in the wave progress update:
1084
+ **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:
1000
1085
  ```bash
1001
1086
  BLOCKED='["rm -rf", "git push --force", "DROP TABLE", "git reset --hard", "git checkout -- ."]'
1002
1087
  # Warning: policy file .orchestrator/policy/blocked-commands.json not found — using legacy hardcoded blocklist
@@ -1008,7 +1093,7 @@ Before each wave dispatch:
1008
1093
  "role": "<role>",
1009
1094
  "enforcement": "<from Session Config, default: warn>",
1010
1095
  "allowedPaths": ["<from agent specs in session plan>"],
1011
- "blockedCommands": "<derived dynamically from .orchestrator/policy/blocked-commands.json (severity: block rules); falls back to legacy 5-element array if policy file absent>",
1096
+ "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>",
1012
1097
  "gates": "<copy of enforcement-gates from Session Config, or omit if unset>"
1013
1098
  }
1014
1099
  ```
@@ -1017,20 +1102,53 @@ Before each wave dispatch:
1017
1102
  3. `allowedPaths` is the UNION of all agent file scopes for this wave
1018
1103
  To compute `allowedPaths`: read each agent's specification from the session plan. Each agent lists its "Files:" scope (e.g., `skills/session-end/SKILL.md`, `scripts/*.sh`). Collect all file paths and glob patterns from all agents in this wave into a single flat array. Deduplicate entries. If an agent's scope uses globs (e.g., `scripts/*.sh`), include the glob pattern as-is — the enforcement hook resolves globs at check time.
1019
1104
 
1105
+ **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 — expand through the shared helper so the hook, the validator and this prose state one rule:
1106
+
1107
+ ```js
1108
+ import { expandTestSiblings } from '$PLUGIN_ROOT/scripts/lib/scope-gate.mjs';
1109
+
1110
+ // unionScopes: the deduplicated flat array from the paragraph above.
1111
+ // role: this wave's role, verbatim from the session plan — the helper GATES on it.
1112
+ const allowedPaths = expandTestSiblings(unionScopes, { role });
1113
+ ```
1114
+
1115
+ 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`.
1116
+
1117
+ > **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.
1118
+
1119
+ **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.
1120
+
1121
+ > 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).
1122
+
1123
+ **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.
1124
+
1125
+ Three ordering constraints, all load-bearing:
1126
+ - Expand each agent's `fileScope` **before** the overlap/deconfliction check, so a test file newly shared by two agents is visible to the check that exists to catch that collision.
1127
+ - 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.
1128
+ - Skip **absolute** entries entirely — expanding a Gate-5b out-of-repo grant would sprout a synthetic `tests/**` sibling outside the repo.
1129
+
1020
1130
  **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`. Write the agent's "Files:" scope as a JSON array of strings to a temp file (`$AGENT_FILESCOPE_JSON`) and run:
1021
1131
 
1022
1132
  ```bash
1023
1133
  node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
1024
- --assert-subset "$AGENT_FILESCOPE_JSON" < <state-dir>/wave-scope.json
1134
+ --assert-subset "$AGENT_FILESCOPE_JSON" --expand-test-siblings \
1135
+ < <state-dir>/wave-scope.json
1025
1136
  ```
1026
1137
 
1138
+ `--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.
1139
+
1140
+ 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.)
1141
+
1027
1142
  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.
1028
1143
  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.
1029
1144
  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."
1030
1145
  > **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.
1146
+ > **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.
1031
1147
  6. For **Quality** role waves, use two-phase scope enforcement:
1032
1148
  - **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.
1149
+ 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.
1033
1150
  - **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.
1151
+ 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.
1034
1152
 
1035
1153
  **Phase transition sequence:**
1036
1154
  1. Compute production file list: `git diff --name-only $SESSION_START_REF..HEAD | grep -v -E '\.(test|spec)\.' | grep -v '__tests__/'`
@@ -56,15 +56,15 @@ absent, skip silently — do not assume any host path.
56
56
  - If `> 0`, surface the count and remind that the session-end auto-commit and
57
57
  `/evolve` handle catch-up. Do **not** commit the vault yourself.
58
58
 
59
- ## 4. Top-3 priority:high backlog
59
+ ## 4. Top-3 priority::high backlog
60
60
 
61
61
  Detect the host from `git remote -v` (as in check 2) and query, without `-R`/owner flags:
62
62
 
63
- - GitLab: `glab issue list --label priority:high --state opened --per-page 3 --output json`
64
- - GitHub: `gh issue list --label priority:high --state open --limit 3`
63
+ - GitLab: `glab issue list --label priority::high --state opened --per-page 3 --output json`
64
+ - GitHub: `gh issue list --label priority::high --state open --limit 3`
65
65
 
66
66
  Surface the issue id + title — up to three lines. Skip silently when the query
67
- returns zero results or the CLI errors. Surface any `priority:high` issue that
67
+ returns zero results or the CLI errors. Surface any `priority::high` issue that
68
68
  exists, even a single one.
69
69
 
70
70
  ## 5. Idle path