session-orchestrator 3.19.0 → 3.21.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 (158) 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 +10 -8
  5. package/CHANGELOG.md +494 -0
  6. package/README.md +16 -11
  7. package/agents/analyst.md +1 -1
  8. package/agents/architect-reviewer.md +1 -1
  9. package/agents/code-implementer.md +4 -2
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/dialectic-deriver.md +1 -1
  12. package/agents/docs-writer.md +1 -1
  13. package/agents/memory-proposal-collector.md +1 -1
  14. package/agents/qa-strategist.md +1 -1
  15. package/agents/security-reviewer.md +1 -1
  16. package/agents/session-reviewer.md +42 -1
  17. package/agents/skill-applied-judge.md +1 -1
  18. package/agents/test-writer.md +1 -1
  19. package/agents/ui-developer.md +1 -1
  20. package/agents/ux-evaluator.md +1 -1
  21. package/commands/release.md +60 -0
  22. package/commands/session.md +6 -2
  23. package/docs/USER-GUIDE.md +1 -1
  24. package/docs/instruction-delivery.md +350 -0
  25. package/docs/migration-v3.md +9 -6
  26. package/docs/persona-panel.md +3 -1
  27. package/docs/scope-collision-guard.md +167 -0
  28. package/docs/session-config-reference.md +1 -41
  29. package/docs/session-config-template.md +0 -23
  30. package/hooks/_lib/guard-source-loader.mjs +304 -91
  31. package/hooks/enforce-commands.mjs +216 -17
  32. package/hooks/enforce-scope.mjs +236 -12
  33. package/hooks/hooks-codex.json +1 -1
  34. package/hooks/hooks.json +11 -1
  35. package/hooks/on-session-end.mjs +52 -5
  36. package/hooks/on-session-start.mjs +7 -4
  37. package/hooks/on-stop.mjs +127 -12
  38. package/hooks/post-bash-write-verify.mjs +8 -32
  39. package/hooks/pre-bash-destructive-guard.mjs +146 -59
  40. package/hooks/pre-bash-sessions-ledger-guard.mjs +493 -66
  41. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  42. package/package.json +2 -2
  43. package/pi/prompts/release.md +12 -0
  44. package/scripts/autopilot.mjs +3 -1
  45. package/scripts/backfill-learnings-from-vault.mjs +967 -0
  46. package/scripts/emit-session.mjs +45 -40
  47. package/scripts/export-hw-learnings.mjs +61 -2
  48. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  49. package/scripts/lib/backlog-scan.mjs +106 -15
  50. package/scripts/lib/build-live-signals.mjs +7 -3
  51. package/scripts/lib/ci-status-banner.mjs +207 -23
  52. package/scripts/lib/command-blocker.mjs +322 -62
  53. package/scripts/lib/git-config-drift.mjs +471 -0
  54. package/scripts/lib/hardening.mjs +9 -9
  55. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  56. package/scripts/lib/io.mjs +193 -7
  57. package/scripts/lib/learnings/affinity.mjs +434 -0
  58. package/scripts/lib/learnings/candidates.mjs +736 -0
  59. package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
  60. package/scripts/lib/learnings/judgment.mjs +782 -0
  61. package/scripts/lib/learnings/kebab.mjs +128 -0
  62. package/scripts/lib/learnings/select.mjs +704 -0
  63. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  64. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  65. package/scripts/lib/named-vault-resolver.mjs +105 -16
  66. package/scripts/lib/peer-cards/schema.mjs +6 -2
  67. package/scripts/lib/reconcile/emitter.mjs +107 -22
  68. package/scripts/lib/reconcile/engine.mjs +9 -15
  69. package/scripts/lib/reconcile/renderer.mjs +141 -25
  70. package/scripts/lib/reconcile/sanitize.mjs +518 -0
  71. package/scripts/lib/reconcile/writer.mjs +134 -1
  72. package/scripts/lib/redact-spans.mjs +89 -0
  73. package/scripts/lib/scope-baseline.mjs +77 -17
  74. package/scripts/lib/scope-gate.mjs +852 -72
  75. package/scripts/lib/secret-masker.mjs +262 -0
  76. package/scripts/lib/session-close-backfill.mjs +2 -2
  77. package/scripts/lib/session-lock.mjs +34 -10
  78. package/scripts/lib/session-record-repair.mjs +551 -0
  79. package/scripts/lib/session-registry.mjs +9 -1
  80. package/scripts/lib/session-schema/serializer.mjs +54 -0
  81. package/scripts/lib/session-schema.mjs +1 -0
  82. package/scripts/lib/session-token-rollup.mjs +68 -6
  83. package/scripts/lib/soul-resolve.mjs +12 -0
  84. package/scripts/lib/state-md/mission-status.mjs +21 -12
  85. package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
  86. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  87. package/scripts/lib/validate/check-agents.mjs +77 -5
  88. package/scripts/lib/validate/check-banner-parity.mjs +376 -0
  89. package/scripts/lib/validate/check-commands.mjs +2 -20
  90. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  91. package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
  92. package/scripts/lib/validate/check-hooks-symmetry.mjs +18 -0
  93. package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
  94. package/scripts/lib/validate/check-owner-leakage.mjs +188 -20
  95. package/scripts/lib/validate/check-rules.mjs +31 -5
  96. package/scripts/lib/validate/check-skills.mjs +191 -0
  97. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  98. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  99. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  100. package/scripts/lib/validate/check-unwired-features.mjs +757 -0
  101. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  102. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  103. package/scripts/lib/validate/tier-inference.mjs +46 -8
  104. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  105. package/scripts/lib/vault-mirror/process.mjs +264 -31
  106. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  107. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  108. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  109. package/scripts/lib/vcs-repo-spec.mjs +500 -19
  110. package/scripts/print-applicable-rules.mjs +170 -7
  111. package/scripts/print-learnings-index.mjs +501 -0
  112. package/scripts/release.mjs +616 -61
  113. package/scripts/repair-invalid-sessions.mjs +209 -0
  114. package/scripts/site-numbers.mjs +1049 -0
  115. package/scripts/sweep-expired-learnings.mjs +192 -32
  116. package/scripts/validate-plugin.mjs +82 -0
  117. package/scripts/validate-wave-scope.mjs +281 -12
  118. package/scripts/vault-mirror.mjs +26 -1
  119. package/skills/_shared/monitor-patterns.md +24 -4
  120. package/skills/_shared/state-ownership.md +17 -0
  121. package/skills/brainstorm/soul.md +47 -1
  122. package/skills/claude-md-drift-check/SKILL.md +9 -1
  123. package/skills/debug/SKILL.md +4 -1
  124. package/skills/discovery/issue-templates.md +4 -4
  125. package/skills/discovery/probes-code.md +2 -2
  126. package/skills/discovery/probes-feature.md +6 -6
  127. package/skills/discovery/probes-infra.md +2 -2
  128. package/skills/discovery/probes-session.md +5 -5
  129. package/skills/dispatcher/SKILL.md +10 -1
  130. package/skills/evolve/SKILL.md +116 -18
  131. package/skills/frontmatter-guard/SKILL.md +9 -1
  132. package/skills/gitlab-ops/SKILL.md +54 -39
  133. package/skills/gitlab-portfolio/SKILL.md +10 -1
  134. package/skills/grill/soul.md +44 -1
  135. package/skills/memory-cleanup/SKILL.md +18 -5
  136. package/skills/npm-publish/SKILL.md +22 -50
  137. package/skills/persona-panel/SKILL.md +3 -1
  138. package/skills/plan/mode-new.md +23 -5
  139. package/skills/plan/soul.md +46 -3
  140. package/skills/repo-audit/SKILL.md +10 -1
  141. package/skills/session-end/SKILL.md +45 -26
  142. package/skills/session-end/metrics-collection.md +1 -1
  143. package/skills/session-end/phase-3-6-tail.md +30 -1
  144. package/skills/session-end/plan-verification.md +1 -5
  145. package/skills/session-end/session-metrics-write.md +6 -10
  146. package/skills/session-plan/SKILL.md +2 -2
  147. package/skills/session-plan/wave-template.md +1 -1
  148. package/skills/session-start/SKILL.md +15 -1
  149. package/skills/session-start/soul.md +41 -1
  150. package/skills/spinout/SKILL.md +5 -1
  151. package/skills/sunset-review/SKILL.md +11 -1
  152. package/skills/tmux-layout/SKILL.md +7 -2
  153. package/skills/vault-mirror/SKILL.md +10 -1
  154. package/skills/vault-sync/SKILL.md +10 -1
  155. package/skills/vault-sync/validator.mjs +55 -6
  156. package/skills/wave-executor/SKILL.md +1 -5
  157. package/skills/wave-executor/wave-loop.md +77 -82
  158. package/scripts/lib/mission-status-schema.mjs +0 -114
@@ -11,7 +11,7 @@ You are the Session Orchestrator — a seasoned engineering lead who has shipped
11
11
  - Bias toward action — do, don't talk
12
12
  - When you see a problem, name it. Don't soften bad news.
13
13
  - Short sentences. No filler. Every word earns its place.
14
- - Communication style: {{tone.style}}. Output level: {{efficiency.output-level}}. Preamble: {{efficiency.preamble}}.
14
+ - How much you say is a dial the operator sets, not a matter of taste — see § Output Levels.
15
15
 
16
16
  ### Have opinions
17
17
  - You are NOT a neutral options-lister. You have preferences shaped by experience.
@@ -31,6 +31,46 @@ You are the Session Orchestrator — a seasoned engineering lead who has shipped
31
31
  - Never catastrophize. Problems are just tasks that haven't been planned yet.
32
32
  - Session end: concrete summary of impact. "Today we moved X forward."
33
33
 
34
+ ## Output Levels
35
+
36
+ The active level is `efficiency.output-level` in `~/.config/session-orchestrator/owner.yaml`. If that file is missing, unreadable, or the key is absent, the level is `full`. Apply the matching block below for the whole session.
37
+
38
+ **How to read a budget.** "Lines before the first question" counts every chat line you author from session start until your first `AskUserQuestion` — raw tool output does not count, your narration of it does. A budget is a ceiling, not a target: under is fine, over is a defect. You meet it by WITHHOLDING, never by dropping — nothing verified disappears, it just waits to be asked for.
39
+
40
+ **Escalation (all levels).** When the operator writes `expand <topic>` (German: `mehr zu <Abschnitt>`), print that topic's full detail immediately, without re-asking and without the budget applying to that one response. Durable detail also stays on disk — `STATE.md`, the wave plan, `.orchestrator/metrics/` — point there rather than reprinting bulk.
41
+
42
+ **Never traded for brevity (all levels).** No budget may be met by cutting any of the following. Where a budget and one of them collide, the budget yields:
43
+ - input validation, and the reporting of invalid input;
44
+ - error handling, error messages, and failure disclosure — a swallowed error is never "concise";
45
+ - security findings, warnings, and destructive-action confirmations (PSA-003);
46
+ - accessibility of the output itself — no meaning carried by colour or emoji alone, no bare unlabelled numbers, no table whose header you dropped to save a line;
47
+ - anything the operator explicitly asked to see.
48
+
49
+ ### output-level: ultra
50
+ - Meaning: telegraphic — decisions, data, and diffs only. No narration.
51
+ - Budget: ≤80 lines before the first question; ≤6 lines per finding; ≤1 line of preamble per tool call.
52
+ - Shape: bullets and tables, no prose paragraphs. Findings as `<severity> <file>:<line> — <what>`. Never restate what a tool just printed; never summarise your own summary.
53
+ - Escalation: `expand <topic>` — see § Escalation above.
54
+
55
+ ### output-level: full
56
+ - Meaning: terse but complete — narration trimmed, data preserved. This is the default.
57
+ - Budget: ≤150 lines before the first question; ≤12 lines per finding; ≤2 lines of preamble per tool call.
58
+ - Shape: one line of rationale per recommendation, then the data. Prose only where a bullet would lose the causal link. Every finding keeps its evidence (command + result); what gets trimmed is the explanation OF the evidence, never the evidence.
59
+ - Escalation: `expand <topic>` — see § Escalation above.
60
+
61
+ ### output-level: lite
62
+ - Meaning: verbose — articles, explanations, and context kept. Chosen for learning, not for speed.
63
+ - Budget: ≤300 lines before the first question; ≤30 lines per finding. Still a ceiling — `lite` is not "unbounded".
64
+ - Shape: explain the WHY behind each recommendation, name the alternatives you rejected and why, spell out unfamiliar terms on first use.
65
+ - Escalation: `expand <topic>` — see § Escalation above.
66
+
67
+ ### Companion dials
68
+
69
+ Same file, same lookup, same fallback-to-default rule:
70
+
71
+ - `efficiency.preamble` — `minimal` (default): at most one clause before a tool call, and only when the next step is non-obvious; never "Let me check X." immediately followed by checking X. `verbose`: one sentence before each tool call naming what you expect to find.
72
+ - `tone.style` — `direct` (this soul's baseline: lead with the recommendation, name problems plainly), `neutral` (state findings without advocacy; still recommend when asked), `friendly` (same content, softer framing; never softer facts).
73
+
34
74
  ## Decision-Making Philosophy
35
75
 
36
76
  When ambiguity arises, resolve it using this hierarchy:
@@ -2,7 +2,11 @@
2
2
  name: spinout
3
3
  user-invocable: true
4
4
  model: sonnet
5
- description: Use when extracting a project into its own repo — a venture spinout (e.g. a product leaving its incubator repo) or a sanitized content-snapshot fork. Guided 5-step runbook: target sphere + path, confidentiality/sanitize check, copy + fresh git init, SNAPSHOT-FREEZE marker in the source repo, remotes + registration. Trigger on 'spin out X', 'extract this into its own repo', 'fork X sanitized'.
5
+ description: >
6
+ Use when extracting a project into its own repo — a venture spinout (e.g. a product leaving its
7
+ incubator repo) or a sanitized content-snapshot fork. Guided 5-step runbook: target sphere + path,
8
+ confidentiality/sanitize check, copy + fresh git init, SNAPSHOT-FREEZE marker in the source repo,
9
+ remotes + registration. Trigger on 'spin out X', 'extract this into its own repo', 'fork X sanitized'.
6
10
  ---
7
11
 
8
12
  # spinout — Guided Project-Extraction Runbook
@@ -1,6 +1,16 @@
1
1
  ---
2
2
  name: sunset-review
3
- description: Use this skill when the user wants to identify unused, near-zero-use, or stale skills/agents/commands in the plugin surface so they can be demoted or retired. Combines agent-dispatch telemetry (start-events only) with static reference scanning, classifies every surface item into Active / Investigate / Demote / Retire, and emits a Markdown report plus JSON sidecar. NEVER auto-deletes — surfaces candidates for human decision. Quarterly cadence. <example>Context: The plugin surface has grown and the maintainer wants to prune dead weight. user: "/sunset-review" assistant: "Running the sunset walk — classifying skills, agents, and commands by usage telemetry + static refs, grouped by Retire / Demote / Investigate / Active. No item is deleted automatically; I'll surface Retire/Demote candidates for your decision." <commentary>The user wants a usage-driven prune candidate list; this skill runs the read-only walker, presents grouped verdicts, and writes a sidecar — it never deletes.</commentary></example>
3
+ description: >
4
+ Use this skill when the user wants to identify unused, near-zero-use, or stale skills/agents/commands in
5
+ the plugin surface so they can be demoted or retired. Combines agent-dispatch telemetry (start-events
6
+ only) with static reference scanning, classifies every surface item into Active / Investigate / Demote /
7
+ Retire, and emits a Markdown report plus JSON sidecar. NEVER auto-deletes — surfaces candidates for
8
+ human decision. Quarterly cadence. <example>Context: The plugin surface has grown and the maintainer
9
+ wants to prune dead weight. user: "/sunset-review" assistant: "Running the sunset walk — classifying
10
+ skills, agents, and commands by usage telemetry + static refs, grouped by Retire / Demote / Investigate
11
+ / Active. No item is deleted automatically; I'll surface Retire/Demote candidates for your decision."
12
+ <commentary>The user wants a usage-driven prune candidate list; this skill runs the read-only walker,
13
+ presents grouped verdicts, and writes a sidecar — it never deletes.</commentary></example>
4
14
  model: inherit
5
15
  color: amber
6
16
  ---
@@ -1,6 +1,11 @@
1
1
  ---
2
2
  name: tmux-layout
3
- description: Use this skill when the operator wants a prepared tmux visualization layout for the session's side-channels (STATE.md tail, CI-watch, events.jsonl tail). Renders a 4-pane default layout or debug layout. Read-only side-channel observability — the coordinator chat stays in the operator's original terminal. Trigger phrases: "tmux layout", "split panes for ci watch", "visualize session side-channels", "show me state-md tail and ci".
3
+ description: >
4
+ Use this skill when the operator wants a prepared tmux visualization layout for the session's
5
+ side-channels (STATE.md tail, CI-watch, events.jsonl tail). Renders a 4-pane default layout or debug
6
+ layout. Read-only side-channel observability — the coordinator chat stays in the operator's original
7
+ terminal. Trigger phrases: "tmux layout", "split panes for ci watch", "visualize session side-channels",
8
+ "show me state-md tail and ci".
4
9
  model: inherit
5
10
  color: cyan
6
11
  tools: Read, Bash, Grep, Glob
@@ -47,7 +52,7 @@ The skill prints a one-line tmux command. Paste it into a SECOND terminal (do no
47
52
  |---|---|---|
48
53
  | 1 | **Shell** (operator scratch — NOT claude) | `bash` (interactive) |
49
54
  | 2 | STATE.md tail | `tail -F <state-dir>/STATE.md` |
50
- | 3 | CI watch (poll-loop wrapper) | `while true; do clear; glab ci status --pipeline-id LATEST --output json \| jq ...; sleep 15; done` |
55
+ | 3 | CI watch (poll-loop wrapper) | `while true; do clear; glab ci status -R <spec> --output json \| jq -r '.jobs[] \| ...'; sleep 15; done` |
51
56
  | 4 | events.jsonl wave/gate filter | `tail -F .orchestrator/metrics/events.jsonl \| jq --unbuffered 'select(.event \| test("wave\|gate\|spiral"))'` |
52
57
  | 5 | agent-status telemetry (#565, only with `--with-status-pane`) | `while true; do clear; jq . .orchestrator/runtime/agent-status-current.json 2>/dev/null \|\| echo ...; sleep 2; done` |
53
58
 
@@ -1,6 +1,15 @@
1
1
  ---
2
2
  name: vault-mirror
3
- description: Use when you need to populate the Meta-Vault with machine-generated notes derived from session-orchestrator JSONL records. Converts entries from `.orchestrator/metrics/sessions.jsonl` and `.orchestrator/metrics/learnings.jsonl` into vault-conformant Markdown under `50-sessions/` and `40-learnings/`. Called automatically at session-end Phase 3.7 and after evolve Phase 3.5 — only when `vault-integration.enabled=true` and `vault-integration.mode != "off"`. Idempotent: re-runs safely; skips hand-authored notes. Triggers: "mirror to vault", "sync session notes to vault", "write learning notes to vault", "vault-mirror failed at session close". <example>Context: session-end is finalizing, vault-integration.mode is "warn". user: "/close" assistant: "Running vault-mirror to write 50-sessions/session-2026-05-17.md from the closing session record — 1 created, 0 skipped."</example>
3
+ description: >
4
+ Use when you need to populate the Meta-Vault with machine-generated notes derived from
5
+ session-orchestrator JSONL records. Converts entries from `.orchestrator/metrics/sessions.jsonl` and
6
+ `.orchestrator/metrics/learnings.jsonl` into vault-conformant Markdown under `50-sessions/` and
7
+ `40-learnings/`. Called automatically at session-end Phase 3.7 and after evolve Phase 3.5 — only when
8
+ `vault-integration.enabled=true` and `vault-integration.mode != "off"`. Idempotent: re-runs safely;
9
+ skips hand-authored notes. Triggers: "mirror to vault", "sync session notes to vault", "write learning
10
+ notes to vault", "vault-mirror failed at session close". <example>Context: session-end is finalizing,
11
+ vault-integration.mode is "warn". user: "/close" assistant: "Running vault-mirror to write
12
+ 50-sessions/session-2026-05-17.md from the closing session record — 1 created, 0 skipped."</example>
4
13
  model: haiku
5
14
  ---
6
15
 
@@ -1,6 +1,15 @@
1
1
  ---
2
2
  name: vault-sync
3
- description: Use when you need to validate the Meta-Vault's Markdown frontmatter and wiki-link integrity before closing a session or after vault edits. Runs as a hard gate at session-end Phase 1 — blocks close if any `.md` file fails the Zod frontmatter schema or has dangling `[[wiki-links]]`. Supports three modes: `hard` (blocks on errors), `warn` (reports without blocking), `off` (skip). Reads `vault-sync.*` from Session Config; respects per-vault exclude globs from `CLAUDE.md`. Triggers: "vault validation failed at session close", "fix vault frontmatter errors", "check vault wiki-links", "why is session-end blocked by vault-sync". <example>Context: session-end Phase 1 quality gate, vault-sync.enabled=true, vault-sync.mode="hard". user: "/close" assistant: "vault-sync found 2 frontmatter errors in vault/40-learnings/ml-notes.md — missing required `id` field. Fixing before close."</example>
3
+ description: >
4
+ Use when you need to validate the Meta-Vault's Markdown frontmatter and wiki-link integrity before
5
+ closing a session or after vault edits. Runs as a hard gate at session-end Phase 1 — blocks close if any
6
+ `.md` file fails the Zod frontmatter schema or has dangling `[[wiki-links]]`. Supports three modes:
7
+ `hard` (blocks on errors), `warn` (reports without blocking), `off` (skip). Reads `vault-sync.*` from
8
+ Session Config; respects per-vault exclude globs from `CLAUDE.md`. Triggers: "vault validation failed at
9
+ session close", "fix vault frontmatter errors", "check vault wiki-links", "why is session-end blocked by
10
+ vault-sync". <example>Context: session-end Phase 1 quality gate, vault-sync.enabled=true,
11
+ vault-sync.mode="hard". user: "/close" assistant: "vault-sync found 2 frontmatter errors in
12
+ vault/40-learnings/ml-notes.md — missing required `id` field. Fixing before close."</example>
4
13
  model: haiku
5
14
  ---
6
15
 
@@ -254,6 +254,14 @@ for (let i = 0; i < args.length; i++) {
254
254
  // ? — any single character except `/`
255
255
  // literal path separators and characters otherwise
256
256
  // Operates on POSIX-style forward-slash relative paths.
257
+ // `**` compiles to a SEGMENT-ANCHORED alternative, not a free `(?:.*?)` (#1013).
258
+ // The free form ended anywhere, including mid-segment, so every `**/` pattern
259
+ // silently grew a suffix-match: `**/README.md` matched `MYREADME.md`, and
260
+ // `**/archive/**` matched `90-archive/...`. This matcher answers "is this path
261
+ // EXCLUDED from validation", so an over-approximation is not a harmless
262
+ // widening — it is a false negative: the file is never checked and the
263
+ // validator still reports 0 errors. Anchoring narrows exclusion, i.e. it can
264
+ // only ever add files to the checked set.
257
265
  function globToRegExp(glob) {
258
266
  // Normalise input
259
267
  const g = glob.replace(/\\/g, '/');
@@ -262,12 +270,18 @@ function globToRegExp(glob) {
262
270
  const c = g[i];
263
271
  if (c === '*') {
264
272
  if (g[i + 1] === '*') {
265
- // `**` match across path segments
266
- // Also swallow a following `/` so that `**/foo` matches `foo` at root.
267
- const nextSlash = g[i + 2] === '/';
268
- re += '(?:.*?)';
269
- if (nextSlash) i += 2;
270
- else i += 1;
273
+ if (g[i + 2] === '/') {
274
+ // `**/` zero or more WHOLE path segments. Zero repetitions is what
275
+ // makes `**/foo` match `foo` at the vault root, and it is why the
276
+ // trailing `/` is swallowed here rather than emitted literally.
277
+ re += '(?:[^/]+/)*';
278
+ i += 2;
279
+ } else {
280
+ // Trailing or standalone `**` (e.g. `a/**`) — everything below the
281
+ // preceding literal, segment boundaries included.
282
+ re += '.*';
283
+ i += 1;
284
+ }
271
285
  } else {
272
286
  re += '[^/]*';
273
287
  }
@@ -658,6 +672,21 @@ const errors = [];
658
672
  const warnings = [];
659
673
  let filesChecked = 0;
660
674
  let filesSkippedNoFrontmatter = 0;
675
+ // The paths behind the count (#1013). A bare count made "0 errors" ambiguous:
676
+ // it meant "everything CHECKED is valid", never "everything is valid", and the
677
+ // operator had no way to see WHICH files were never checked. The list is
678
+ // emitted only by the branches that actually ran the validation pass — so an
679
+ // ABSENT `files_skipped_no_frontmatter_paths` means "never inspected" (mode=off,
680
+ // no vault) while an EMPTY one means "inspected, nothing skipped". Same
681
+ // absence-preserving contract as `scripts/lib/reconcile-nudge-banner.mjs`
682
+ // (`skippedCandidates`), which distinguishes never-checked from checked-clean.
683
+ const filesSkippedNoFrontmatterPaths = [];
684
+ // Ceiling: 50 paths keeps the envelope bounded on a vault that has never been
685
+ // backfilled (real measurement 2026-08-15: 2 of 8574 files in the live vault).
686
+ // `paths.length < files_skipped_no_frontmatter` is itself the truncation
687
+ // signal, so no extra flag is needed. Revisit if a consuming vault routinely
688
+ // reports more than 50 — then the right fix is a backfill, not a bigger cap.
689
+ const SKIPPED_PATHS_CAP = 50;
661
690
  let excludedCount = 0;
662
691
  let archivedSkippedCount = 0;
663
692
 
@@ -686,6 +715,9 @@ for (const rec of records) {
686
715
 
687
716
  if (!fm.hasFrontmatter) {
688
717
  filesSkippedNoFrontmatter++;
718
+ if (filesSkippedNoFrontmatterPaths.length < SKIPPED_PATHS_CAP) {
719
+ filesSkippedNoFrontmatterPaths.push(rel);
720
+ }
689
721
  continue;
690
722
  }
691
723
 
@@ -737,6 +769,19 @@ for (const rec of records) {
737
769
 
738
770
  const hasErrors = errors.length > 0;
739
771
 
772
+ // Silence is not success: a skipped file contributes to neither `errors` nor
773
+ // `files_checked`, so a bare "0 errors" line would let an unvalidated file pass
774
+ // unremarked at the session-end hard gate (#1013). Surface the count on stderr
775
+ // too — stdout stays pure JSON per the caller contract.
776
+ if (filesSkippedNoFrontmatter > 0) {
777
+ const shown = filesSkippedNoFrontmatterPaths.slice(0, 5).join(', ');
778
+ const more = filesSkippedNoFrontmatter - Math.min(5, filesSkippedNoFrontmatterPaths.length);
779
+ process.stderr.write(
780
+ `WARN: ${filesSkippedNoFrontmatter} file(s) NOT validated — no frontmatter: ` +
781
+ `${shown}${more > 0 ? ` (+${more} more)` : ''}\n`,
782
+ );
783
+ }
784
+
740
785
  // ── mode=baseline ─────────────────────────────────────────────────────────
741
786
  // Serialize current errors + warnings as a snapshot, then exit 0.
742
787
  if (mode === 'baseline') {
@@ -774,6 +819,7 @@ if (mode === 'diff') {
774
819
  excluded_count: excludedCount,
775
820
  archived_skipped_count: archivedSkippedCount,
776
821
  files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
822
+ files_skipped_no_frontmatter_paths: filesSkippedNoFrontmatterPaths,
777
823
  errors,
778
824
  warnings,
779
825
  });
@@ -794,6 +840,7 @@ if (mode === 'diff') {
794
840
  excluded_count: excludedCount,
795
841
  archived_skipped_count: archivedSkippedCount,
796
842
  files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
843
+ files_skipped_no_frontmatter_paths: filesSkippedNoFrontmatterPaths,
797
844
  errors,
798
845
  warnings,
799
846
  });
@@ -816,6 +863,7 @@ if (mode === 'diff') {
816
863
  excluded_count: excludedCount,
817
864
  archived_skipped_count: archivedSkippedCount,
818
865
  files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
866
+ files_skipped_no_frontmatter_paths: filesSkippedNoFrontmatterPaths,
819
867
  });
820
868
 
821
869
  process.stderr.write(
@@ -839,6 +887,7 @@ emit({
839
887
  excluded_count: excludedCount,
840
888
  archived_skipped_count: archivedSkippedCount,
841
889
  files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
890
+ files_skipped_no_frontmatter_paths: filesSkippedNoFrontmatterPaths,
842
891
  errors,
843
892
  warnings,
844
893
  });
@@ -245,7 +245,7 @@ Each agent prompt MUST include:
245
245
  1. **Clear scope boundary**: "You are working on [X]. Do NOT modify files outside [paths]."
246
246
  2. **Full context**: file paths, current code structure, issue description. If a bite-sized executable plan exists at `docs/plans/<feature>.md` for the wave's tasks (see `skills/write-executable-plan/SKILL.md`), include the path in each agent's prompt and instruct the agent to follow the plan's 5-step structure verbatim.
247
247
  3. **Acceptance criteria**: measurable definition of done
248
- 4. **Rule references**: the wave's applicable rules are injected automatically as the `<APPLICABLE-RULES>` block produced by `scripts/print-applicable-rules.mjs` (see `wave-loop.md` § "Pre-Dispatch: Glob-Scoped Rule Injection (#336/#694)"). The block is computed once per wave from the wave's `allowedPaths` and prepended to every agent prompt — do not hand-copy rule paths into the prompt.
248
+ 4. **Rule references**: the wave's applicable rules are injected automatically as the `<APPLICABLE-RULES>` block produced by `scripts/print-applicable-rules.mjs` (see `wave-loop.md` § "Pre-Dispatch: Glob-Scoped Rule Injection (#336/#694)"). The block is computed once per wave from the wave's `allowedPaths` and prepended to every agent prompt — do not hand-copy rule paths into the prompt. Past **learnings** arrive separately as the `<LEARNINGS-INDEX>` block from `scripts/print-learnings-index.mjs` (see `wave-loop.md` § "Pre-Dispatch: Learnings-Index Injection (#1014)"), computed **per agent** from its own file scope rather than once per wave.
249
249
  5. **Testing expectation** (need-gated): "Before writing any test, name the concrete bug a NEW test would catch that the existing suite does not. No nameable bug → write NO test and report `no-tests-needed: <reason>` — that is a SUCCESS outcome, not a gap. With a nameable bug: exactly one test for it. Running existing tests is always mandatory."
250
250
  6. **Commit instruction**: "Do NOT commit. The coordinator handles commits."
251
251
  7. **Turn limit**: Include the maxTurns instruction from `circuit-breaker.md`
@@ -438,10 +438,6 @@ const result = await runQualityGateWithRetry({
438
438
  Gherkin negative path).
439
439
  - `verification-auto-fix.max-retries: 0` → equivalent to disabled.
440
440
 
441
- ### /goal Continuation Anchor (opt-in — #636)
442
-
443
- When `goal-integration.enabled: true` with seam `inter-wave-fixloop` in Session Config, the coordinator may surface ONE advisory `/goal` command at the inter-wave fix-loop seam to anchor continuation across the auto-fix retries — see `wave-loop.md` § "##### /goal Continuation Anchor" for the gate conditions, suggested command, and the LM-008 cross-reference. The advisory never alters gate semantics: `runQualityGateWithRetry()`'s exit-code result remains the judgment, and the hard-abort + diagnostics-bundle path after `max-retries` is unchanged. Default off → zero behaviour change.
444
-
445
441
  ### Anti-pattern (BE-012 awareness)
446
442
 
447
443
  The fixer-agent prompt MUST include a reminder of `.claude/rules/testing.md` § "Test Quality — False-Positive Prevention"
@@ -263,6 +263,7 @@ For each agent in this wave:
263
263
  - Which files to read/modify (exact paths)
264
264
  - Acceptance criteria (how to verify done)
265
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)
266
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)
267
268
  - VCS issue reference if applicable
268
269
  - What NOT to touch (other agents' files)
@@ -405,7 +406,7 @@ After `wave-scope.json` is written for this wave and before assembling the `Agen
405
406
  RULES_BLOCK="$(node "$PLUGIN_ROOT/scripts/print-applicable-rules.mjs" --context wave 2>/dev/null)"
406
407
 
407
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:
408
- - 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
409
410
  - empty output (exit 0) when no rules match — in which case prepend nothing.
410
411
 
411
412
  **Prompt assembly:** when `$RULES_BLOCK` is non-empty, prepend it to EACH agent's prompt in this wave under a clear separator:
@@ -420,6 +421,55 @@ When `$RULES_BLOCK` is empty (no `.claude/rules/`, no matching rules, or any CLI
420
421
 
421
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.
422
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` — `<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.
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
+
458
+ #### Pre-Dispatch: File-Scope Injection (#1020)
459
+
460
+ > **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.
461
+
462
+ **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:
463
+
464
+ FILE-SCOPE — exactly these:
465
+ ```
466
+ <one path or glob per line, verbatim from that agent's scope file>
467
+ ```
468
+
469
+ 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.
470
+
471
+ > **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.
472
+
423
473
  #### Structured Reasoning (STATE:/PLAN:) — opt-in via `reasoning-output: true` (#79)
424
474
 
425
475
  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.
@@ -667,29 +717,6 @@ Per attempt:
667
717
  See `SKILL.md` § "Inter-Wave Quality-Gate (with Auto-Fix Loop — #521)" for
668
718
  the full invocation pattern.
669
719
 
670
- ##### /goal Continuation Anchor (opt-in — #636)
671
-
672
- > 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.
673
-
674
- **Gate conditions** — ALL must be true for this nudge to surface:
675
-
676
- 1. `goal-integration.enabled: true` in Session Config (default: `false`).
677
- 2. `inter-wave-fixloop` is listed in `goal-integration.seams`.
678
-
679
- When any gate condition is false, skip this step entirely — proceed to `##### STATE.md Deviation — Auto-Fix Result`.
680
-
681
- **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:
682
-
683
- ```
684
- /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.
685
- ```
686
-
687
- **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.
688
-
689
- 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.
690
-
691
- **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.
692
-
693
720
  ##### STATE.md Deviation — Auto-Fix Result
694
721
 
695
722
  After `runQualityGateWithRetry()` returns:
@@ -1037,53 +1064,6 @@ When the hook is skipped (gate condition false), omit the `persona_gate` field e
1037
1064
 
1038
1065
  **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.
1039
1066
 
1040
- ### 3c. Strategic Compact-Nudge (#620)
1041
-
1042
- > Advisory-only checkpoint. Never auto-compacts. `/compact` is a user slash-command; the coordinator/operator decides when to invoke it.
1043
-
1044
- **Gate conditions** — ALL must be true for the nudge to emit:
1045
-
1046
- 1. `compact-nudge.enabled: true` in Session Config (default: `false`).
1047
- 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.
1048
- 3. `compact-nudge.mode !== 'off'` (when `mode: 'off'` the nudge is a silent no-op even when `enabled: true`).
1049
-
1050
- When any gate condition is false, skip this step entirely — proceed to `### 4. Progress Update`.
1051
-
1052
- **Nudge format** — when the gate fires, append ONE advisory bullet to the wave progress update (step `### 4`):
1053
-
1054
- ```
1055
- - 💡 Compact checkpoint: Wave N (<Role>) complete — consider /compact before Wave N+1 (<NextRole>) to free context (advisory only; see decision table). Never auto-compacts.
1056
- ```
1057
-
1058
- **What survives `/compact` vs what is lost:**
1059
-
1060
- | Survives | Lost |
1061
- |---|---|
1062
- | 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 |
1063
-
1064
- 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.
1065
-
1066
- **Decision table:**
1067
-
1068
- | Wave boundary (completed → next) | Compact? | Why |
1069
- |---|---|---|
1070
- | Discovery → Impl-Core | Yes — **but only after** Discovery's repo-state facts are written into the plan in annotated form (value + measurement command + `measured_at`) | Research/audit context is bulky and the plan + wave-scope.json is the distilled output. But compacting discards the raw evidence and leaves the briefing text as the only source of truth — the structural amplifier of the #908 damage. Un-annotated facts: re-measure or record them first, else **No**. See Pre-Dispatch: Fact-Staleness Annotation. |
1071
- | Impl-Core → Impl-Polish (long Core) | Maybe | Compact only if Polish targets different files; keep if Polish builds on Core's changes. |
1072
- | Impl-Polish → Quality | No | Quality references the just-written code; losing it is costly. |
1073
- | Quality → Finalization | No | Finalization needs the full session diff. |
1074
- | Mid-implementation (within a wave) | No | Losing file paths + partial state is expensive. |
1075
- | After a FAILED/aborted wave | Yes | Clear the dead-end reasoning before the adapted retry. |
1076
- | Switching to an unrelated task block (deep session) | Yes | Debug/exploration traces pollute unrelated downstream work. |
1077
-
1078
- **Behaviour by mode:**
1079
-
1080
- | `mode` | Action |
1081
- |--------|--------|
1082
- | `off` | No nudge (gate condition above). |
1083
- | `warn` | Emit the advisory bullet in the wave progress update. Coordinator/operator acts at their discretion. |
1084
-
1085
- The nudge is informational only — no AskUserQuestion, no state-md write, no sidecar. This step never blocks forward progress.
1086
-
1087
1067
  ### 4. Progress Update
1088
1068
 
1089
1069
  After each wave, provide a brief status:
@@ -1134,19 +1114,34 @@ Before each wave dispatch:
1134
1114
  ```
1135
1115
  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.
1136
1116
  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.
1137
- 3. `allowedPaths` is the UNION of all agent file scopes for this wave
1138
- 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.
1117
+ 3. **`allowedPaths` is COMPUTED from per-agent scope files — never hand-transcribed (#1020).** Transcribing the union by hand produced 5 scope divergences in ONE session. Three steps, in this order; none of them is a judgement call. Globs stay verbatim (`scripts/*.sh`) — the enforcement hook resolves them at check time.
1139
1118
 
1140
- **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:
1119
+ **3.1 one file per agent.** Write each agent's "Files:" scope from the session plan, verbatim, as a JSON array of strings to `<state-dir>/filescopes/wave-<N>/<agent-id>.json`. That path IS `$AGENT_FILESCOPE_JSON` — the same file `--assert-subset` (#796 below), Grounding Injection (#85), the Learnings-Index (#1014) and the File-Scope Injection (#1020) already consume. Do not write a second copy anywhere, and **never to a `$TMPDIR` temp path**: the injector and `hooks/pre-task-scope-disjoint.mjs` need an addressable, wave-keyed location that a temp file cannot be. Reading `$AGENT_FILESCOPE_JSON` as "some temp file" is the one failure that costs no error the injector finds nothing, no `FILE-SCOPE` block reaches the prompt, `extractScopeFromPrompt` returns `[]`, and the dispatch is ALLOWed exactly as it was before #1020, signal-free. The coordinator's OWN planned direct edits go into `<state-dir>/filescopes/wave-<N>/coordinator.json` in the identical form and take part in both steps below: 2 of those 5 divergences were coordinator-direct edits, for which no agent scope file exists by construction, and the commit guard caught them only at the commit boundary.
1141
1120
 
1142
- ```js
1143
- import { expandTestSiblings } from '$PLUGIN_ROOT/scripts/lib/scope-gate.mjs';
1121
+ > **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs BEFORE the union of 3.3 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.
1122
+
1123
+ **3.2 — assert disjointness BEFORE computing the union.** Build the sidecar — an ARRAY of `{id, files}` records (never an object map: a duplicated agent id must stay visible), one record per file written in 3.1, `coordinator.json` included — and run:
1124
+
1125
+ ```bash
1126
+ node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
1127
+ --assert-disjoint "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
1128
+ ```
1129
+
1130
+ Exit 1 (one stderr message per collision) means two agents were handed the same file: fix the session plan, rewrite the affected 3.1 files, 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.
1144
1131
 
1145
- // unionScopes: the deduplicated flat array from the paragraph above.
1146
- // role: this wave's role, verbatim from the session plan — the helper GATES on it.
1147
- const allowedPaths = expandTestSiblings(unionScopes, { role });
1132
+ **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:
1133
+
1134
+ ```bash
1135
+ node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
1136
+ --union "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
1148
1137
  ```
1149
1138
 
1139
+ 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.
1140
+
1141
+ **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.
1142
+
1143
+ **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.
1144
+
1150
1145
  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`.
1151
1146
 
1152
1147
  > **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.
@@ -1158,11 +1153,11 @@ Before each wave dispatch:
1158
1153
  **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.
1159
1154
 
1160
1155
  Three ordering constraints, all load-bearing:
1161
- - 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.
1156
+ - 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.
1162
1157
  - 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.
1163
1158
  - Skip **absolute** entries entirely — expanding a Gate-5b out-of-repo grant would sprout a synthetic `tests/**` sibling outside the repo.
1164
1159
 
1165
- **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:
1160
+ **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:
1166
1161
 
1167
1162
  ```bash
1168
1163
  node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
@@ -1193,4 +1188,4 @@ Before each wave dispatch:
1193
1188
  5. Delete `<state-dir>/wave-scope.json`
1194
1189
  6. Write Phase 2 wave-scope.json with test file allowedPaths (`**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`)
1195
1190
  7. Dispatch test/review agents
1196
- 7. After the final wave completes, delete `<state-dir>/wave-scope.json` (cleanup)
1191
+ 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.