session-orchestrator 5.7.0 → 5.9.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 (202) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -0
  2. package/.agents/skills/architecture/agents/openai.yaml +5 -0
  3. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -0
  4. package/.agents/skills/eval/SKILL.md +1 -1
  5. package/.agents/skills/frontmatter-guard/SKILL.md +1 -0
  6. package/.agents/skills/gitlab-portfolio/SKILL.md +1 -0
  7. package/.agents/skills/hook-development/SKILL.md +1 -0
  8. package/.agents/skills/mcp-builder/SKILL.md +1 -0
  9. package/.agents/skills/tmux-layout/SKILL.md +3 -0
  10. package/.agents/skills/tmux-layout/agents/openai.yaml +5 -0
  11. package/.agents/skills/vault-mirror/SKILL.md +1 -0
  12. package/.agents/skills/vault-sync/SKILL.md +1 -0
  13. package/.agents/skills/write-executable-plan/SKILL.md +3 -0
  14. package/.agents/skills/write-executable-plan/agents/openai.yaml +5 -0
  15. package/.claude-plugin/marketplace.json +1 -1
  16. package/.claude-plugin/plugin.json +1 -1
  17. package/.codex-plugin/plugin.json +1 -1
  18. package/.codex-plugin/skills/architecture/SKILL.md +3 -0
  19. package/.codex-plugin/skills/architecture/agents/openai.yaml +5 -0
  20. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -0
  21. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  22. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +1 -0
  23. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +1 -0
  24. package/.codex-plugin/skills/hook-development/SKILL.md +1 -0
  25. package/.codex-plugin/skills/mcp-builder/SKILL.md +1 -0
  26. package/.codex-plugin/skills/tmux-layout/SKILL.md +3 -0
  27. package/.codex-plugin/skills/tmux-layout/agents/openai.yaml +5 -0
  28. package/.codex-plugin/skills/vault-mirror/SKILL.md +1 -0
  29. package/.codex-plugin/skills/vault-sync/SKILL.md +1 -0
  30. package/.codex-plugin/skills/write-executable-plan/SKILL.md +3 -0
  31. package/.codex-plugin/skills/write-executable-plan/agents/openai.yaml +5 -0
  32. package/.cursor/commands/architecture.md +13 -0
  33. package/.cursor/commands/eval.md +1 -1
  34. package/.cursor/commands/tmux-layout.md +13 -0
  35. package/.cursor/commands/write-executable-plan.md +13 -0
  36. package/.cursor/skills/architecture/SKILL.md +0 -1
  37. package/.cursor/skills/eval/SKILL.md +1 -1
  38. package/.cursor/skills/tmux-layout/SKILL.md +0 -1
  39. package/.cursor/skills/write-executable-plan/SKILL.md +0 -1
  40. package/.cursor-plugin/plugin.json +1 -1
  41. package/CHANGELOG.md +68 -0
  42. package/README.md +13 -9
  43. package/agents/eval-judge.md +5 -5
  44. package/docs/baseline.md +4 -3
  45. package/docs/codex-setup.md +6 -4
  46. package/docs/components.md +5 -4
  47. package/docs/events-schema.md +45 -4
  48. package/docs/github-mirror-protection.md +3 -2
  49. package/docs/install.md +39 -3
  50. package/docs/pi-setup.md +1 -1
  51. package/docs/rule-authoring.md +2 -2
  52. package/docs/session-config-reference.md +7 -5
  53. package/docs/session-config-template.md +3 -2
  54. package/evals/README.md +60 -0
  55. package/evals/instruction-ablation/README.md +147 -0
  56. package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
  57. package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
  58. package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
  59. package/evals/instruction-ablation/run.mjs +336 -0
  60. package/evals/psa-003-foreign-work/case.yaml +16 -0
  61. package/evals/psa-003-foreign-work/fixture.sh +12 -0
  62. package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
  63. package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
  64. package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
  65. package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
  66. package/evals/psa-003-foreign-work/prompt.md +6 -0
  67. package/hooks/_lib/hook-import-set.json +10 -2
  68. package/hooks/hooks-codex.json +1 -1
  69. package/hooks/hooks.json +1 -1
  70. package/hooks/post-tool-batch-wave-signal.mjs +1 -0
  71. package/hooks/post-tooluse-frontend-slop.mjs +11 -1
  72. package/hooks/pre-task-scope-disjoint.mjs +8 -6
  73. package/monitors/monitors.json +1 -1
  74. package/package.json +3 -1
  75. package/pi/prompts/architecture.md +11 -0
  76. package/pi/prompts/eval.md +1 -1
  77. package/pi/prompts/tmux-layout.md +11 -0
  78. package/pi/prompts/write-executable-plan.md +11 -0
  79. package/scripts/backfill-abandoned-sessions.mjs +2 -2
  80. package/scripts/backfill-sessions.mjs +9 -7
  81. package/scripts/codex-install.mjs +108 -23
  82. package/scripts/emit-session.mjs +15 -2
  83. package/scripts/fleet-auflagen.mjs +54 -0
  84. package/scripts/generate-agents-skills.mjs +9 -16
  85. package/scripts/generate-codex-skills.mjs +8 -6
  86. package/scripts/generate-cursor-adapter.mjs +9 -74
  87. package/scripts/generate-pi-prompts.mjs +8 -74
  88. package/scripts/github-protection-audit.mjs +8 -0
  89. package/scripts/lib/auto-dialectic.mjs +10 -9
  90. package/scripts/lib/auto-dream.mjs +8 -10
  91. package/scripts/lib/autopilot/telemetry.mjs +12 -6
  92. package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
  93. package/scripts/lib/ci-status-banner.mjs +38 -5
  94. package/scripts/lib/config/drift-check.mjs +1 -1
  95. package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
  96. package/scripts/lib/config.mjs +5 -1
  97. package/scripts/lib/convergence-monitor.mjs +5 -3
  98. package/scripts/lib/ecosystem-health.mjs +5 -3
  99. package/scripts/lib/eval/engine.mjs +61 -27
  100. package/scripts/lib/eval/judge.mjs +14 -8
  101. package/scripts/lib/eval/session-resolve.mjs +7 -2
  102. package/scripts/lib/events-retention-banner.mjs +5 -2
  103. package/scripts/lib/events-schema.mjs +8 -0
  104. package/scripts/lib/events.mjs +19 -5
  105. package/scripts/lib/fleet-protocol.mjs +98 -1
  106. package/scripts/lib/instruction-budget-guard.mjs +25 -18
  107. package/scripts/lib/io.mjs +172 -35
  108. package/scripts/lib/learnings/candidates.mjs +7 -28
  109. package/scripts/lib/learnings/io.mjs +7 -2
  110. package/scripts/lib/learnings/judgment.mjs +5 -8
  111. package/scripts/lib/learnings/select.mjs +3 -3
  112. package/scripts/lib/maintenance-due-banner.mjs +2 -2
  113. package/scripts/lib/memory-proposals/sink.mjs +5 -6
  114. package/scripts/lib/memory-proposals/store.mjs +3 -5
  115. package/scripts/lib/mirror-issues-banner.mjs +11 -1
  116. package/scripts/lib/native-command.mjs +48 -0
  117. package/scripts/lib/orphan-reaper/scan.mjs +29 -3
  118. package/scripts/lib/peer-cards/writer.mjs +6 -18
  119. package/scripts/lib/platform.mjs +113 -4
  120. package/scripts/lib/plugin-package-stage.mjs +169 -0
  121. package/scripts/lib/plugin-surface.mjs +121 -0
  122. package/scripts/lib/quality-gate-wave.mjs +40 -0
  123. package/scripts/lib/quality-gate.mjs +36 -30
  124. package/scripts/lib/reconcile/idempotency.mjs +9 -10
  125. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +48 -11
  126. package/scripts/lib/reconcile/writer.mjs +11 -17
  127. package/scripts/lib/rule-loader.mjs +32 -6
  128. package/scripts/lib/scope-echo.mjs +12 -2
  129. package/scripts/lib/scope-gate.mjs +2 -1
  130. package/scripts/lib/session-close-backfill.mjs +3 -4
  131. package/scripts/lib/session-id.mjs +2 -1
  132. package/scripts/lib/session-record-repair.mjs +28 -21
  133. package/scripts/lib/session-schema/constants.mjs +20 -5
  134. package/scripts/lib/session-schema/validator.mjs +12 -0
  135. package/scripts/lib/session-start-probes.mjs +22 -7
  136. package/scripts/lib/sessions-staleness-banner.mjs +2 -3
  137. package/scripts/lib/skill-evolution/engine.mjs +6 -9
  138. package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
  139. package/scripts/lib/slopcheck.mjs +9 -7
  140. package/scripts/lib/sunset/walker.mjs +3 -3
  141. package/scripts/lib/telemetry/sync.mjs +3 -1
  142. package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
  143. package/scripts/lib/test-sibling-coverage.mjs +167 -0
  144. package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
  145. package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
  146. package/scripts/lib/user-invocable-skills.mjs +37 -10
  147. package/scripts/lib/ux-grill/run-record.mjs +5 -3
  148. package/scripts/lib/validate/check-banner-parity.mjs +1 -0
  149. package/scripts/lib/validate/check-skills.mjs +12 -2
  150. package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
  151. package/scripts/lib/validate/check-test-value-bans.mjs +3 -2
  152. package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
  153. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  154. package/scripts/lib/vault-archive.mjs +2 -4
  155. package/scripts/lib/vault-mirror/process.mjs +207 -26
  156. package/scripts/lib/vault-mirror/utils.mjs +16 -1
  157. package/scripts/lib/vault-sync-baseline.mjs +9 -8
  158. package/scripts/lib/wave-executor/remote-dispatch.mjs +27 -9
  159. package/scripts/lib/wave-resource-gate.mjs +2 -1
  160. package/scripts/lib/wave-transcript-tail.mjs +8 -4
  161. package/scripts/lib/worktree/meta.mjs +7 -3
  162. package/scripts/mcp-server.mjs +158 -0
  163. package/scripts/migrate-sessions-jsonl.mjs +7 -5
  164. package/scripts/migrate-vault-paths.mjs +7 -4
  165. package/scripts/print-learnings-index.mjs +2 -1
  166. package/scripts/release.mjs +4 -1
  167. package/scripts/relocate-vault-corpus.mjs +5 -3
  168. package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
  169. package/scripts/run-quality-gate.mjs +9 -39
  170. package/scripts/self-update.mjs +179 -39
  171. package/scripts/site-numbers.mjs +8 -3
  172. package/scripts/sweep-expired-learnings.mjs +6 -4
  173. package/scripts/sync-vault-schema.mjs +3 -7
  174. package/scripts/windows-setup.mjs +45 -0
  175. package/skills/_shared/fleet-protocol.md +10 -0
  176. package/skills/architecture/SKILL.md +1 -0
  177. package/skills/bootstrap/_shared-template.md +5 -2
  178. package/skills/claude-md-drift-check/SKILL.md +4 -1
  179. package/skills/claude-md-drift-check/checker.mjs +83 -10
  180. package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
  181. package/skills/eval/SKILL.md +12 -13
  182. package/skills/eval/rubric-v3.md +521 -0
  183. package/skills/frontmatter-guard/SKILL.md +1 -0
  184. package/skills/gitlab-portfolio/SKILL.md +1 -0
  185. package/skills/hook-development/SKILL.md +1 -0
  186. package/skills/mcp-builder/SKILL.md +1 -0
  187. package/skills/remote-offload/SKILL.md +1 -1
  188. package/skills/repo-audit/SKILL.md +2 -2
  189. package/skills/session-end/SKILL.md +32 -1
  190. package/skills/session-end/metrics-collection.md +15 -9
  191. package/skills/session-end/vault-operations.md +12 -2
  192. package/skills/session-start/SKILL.md +2 -2
  193. package/skills/session-start/phase-7-1-premise-check.md +2 -1
  194. package/skills/session-start/references/operations-contract.md +4 -1
  195. package/skills/tmux-layout/SKILL.md +1 -0
  196. package/skills/vault-mirror/SKILL.md +1 -0
  197. package/skills/vault-sync/SKILL.md +6 -6
  198. package/skills/vault-sync/validator.sh +29 -10
  199. package/skills/wave-executor/references/wave-loop-dispatch.md +1 -1
  200. package/skills/wave-executor/references/wave-loop-review.md +13 -7
  201. package/skills/wave-executor/references/wave-loop-scope-manifest.md +4 -4
  202. package/skills/write-executable-plan/SKILL.md +1 -0
@@ -172,19 +172,25 @@ Finalize session metrics by reading the wave data accumulated during execution:
172
172
  > - `effectiveness`: ALWAYS populated from Phase 1 plan verification results, and CONSTRUCTED EXPLICITLY in the METRICS_ENTRY snippet (#773) — never deferred to a "remember to add" optional step (that omission is how `carryover: 0` slipped past 41 records). `completion_rate` = `completed / planned_issues` (0.0-1.0, where 0.0 means nothing was completed). **`carryover` counting rule (#773):** `carryover` is the **length of the Phase 1.65 gate carry-list** — `autoCarry` ∪ the middle-band `ask` items the operator LEFT SELECTED ∪ the answered-question `impliesWork: true` candidates — NOT the raw Phase 1.2+1.3 candidate count. On the fail-open skip (gate disabled / headless / AUQ unavailable), EVERY candidate carries, so `carryover` = the full candidate-list length. Count the gate's OUTPUT (what reaches Phase 5 Step 3 filing), not its INPUT.
173
173
  > - `effectiveness.override_ratio` (#730/H5): OPTIONAL nested field = `overridden_findings / max(total_findings_surfaced, 1)` (float 0.0-1.0). Populate ONLY when Phase 2.6 (Broken-Window Budget) ran this session (`broken-window-budget.enabled: true`). OMIT (do NOT write null/0) otherwise — **absent = "not measured"**, `0.0` = "measured, nothing overridden". `overridden_findings` = the summed `count` of the `orchestrator.finding.overridden` events emitted this session; `total_findings_surfaced` = every MED/LOW+ finding surfaced across Phase 1.8 + wave reviewers.
174
174
  > - `waves[].agent_count_planned` / `waves[].agent_count_started` / `waves[].agent_count_completed` (#724/#1115): OPTIONAL per-wave fields, sourced from `wave-loop.md` § Capture wave metrics step 7 — mirror its definitions exactly, do not re-derive them here. `agent_count_planned` = agents named in the session plan for this wave. `agent_count_started` = distinct agents whose `agent-<id>.meta.json` sidecar is present, after any silent-drop re-dispatch — NOT "produced a tool-result" (under background dispatch the launch ack is a result and would count an agent that never ran). `agent_count_completed` = distinct agents whose task-notification (`<status>completed</status>`) arrived. Omit each field when the wave did not measure it — **absent = "not measured"**, never zero-fill; `0` would read as "measured, no agent started", which is the opposite of an unmeasured wave. The two gaps carry the diagnosis: `agent_count_planned > agent_count_started` after re-dispatch is a persistent silent drop, `agent_count_started > agent_count_completed` at wave end is an agent that started and never returned. Both are also logged to STATE.md `## Deviations` by wave-loop.md, so a record and a deviation entry should agree.
175
- > - `waves[].suite_passed` / `waves[].suite_failed` / `waves[].suite_platform` (#944): OPTIONAL per-wave fields. Omit all three when absent — absent = "not measured", `suite_failed: 0` = "measured, zero failures".
176
- > **`suite_passed` / `suite_failed`: read the event FIRST, the STATE.md header only as fallback (#966 step 3).** Since #954/#967 the between-waves gate wrapper `scripts/run-quality-gate.mjs` emits `orchestrator.quality_gate.{passed,failed}` with a machine-measured `counts: {passed, failed, total}` AND the `wave_number` it resolved from the `wave-scope.json` sidecar, so per-wave attribution needs no wall-clock window join. Payload fields are flat at the record's top level; for each wave `N` of this session:
175
+ > - `waves[].suite_passed` / `waves[].suite_failed` / `waves[].suite_platform` (#944): OPTIONAL per-wave fields. Treat counts and header platform independently; absent = "not measured", `suite_failed: 0` = "measured, zero failures".
176
+ > **`suite_passed` / `suite_failed`: read the event FIRST, the STATE.md header only as fallback (#966 step 3).** Since #954/#967 the between-waves gate wrapper `scripts/run-quality-gate.mjs` emits `orchestrator.quality_gate.{passed,failed}` with a machine-measured `counts: {passed, failed, total}` AND the `wave_number` it resolved from the `wave-scope.json` sidecar, so per-wave attribution needs no wall-clock window join. Payload fields are flat at the record's top level; for each wave of this session:
177
177
  >
178
178
  > ```bash
179
- > jq -c --argjson w N --arg s "<semantic_session_id>" '
180
- > select(.event | startswith("orchestrator.quality_gate."))
181
- > | select(.semantic_session_id == $s and .wave_number == $w and .counts != null)
182
- > | .counts' .orchestrator/metrics/events.jsonl | tail -1
179
+ > jq -cR --argjson w "$WAVE" --arg s "$SEMANTIC_SESSION_ID" '
180
+ > fromjson? | objects
181
+ > | select((.event | type) == "string" and (.event | startswith("orchestrator.quality_gate.")))
182
+ > | select(.semantic_session_id == $s and .wave_number == $w and .["counts"] != null)
183
+ > | .["counts"]
184
+ > ' .orchestrator/metrics/events.jsonl | tail -1
183
185
  > ```
184
186
  >
185
- > Filtering by `semantic_session_id` is mandatory — `events.jsonl` accumulates across sessions and every past session also had a wave `N`. Take the LAST matching record (the wave's final gate run); `counts.passed` → `suite_passed`, `counts.failed` → `suite_failed`. No match = the field was not measured for that wave → omit, never zero-fill (the producer already omits `counts` rather than zero-filling when a run fail-fast'd before the test gate).
186
- > **Fallback, still live:** when no event matches, fall back to the STATE.md Wave History header `— suite <passed>/<failed> on <platform>` (written by wave-executor §3a since #944). Three cases genuinely need it: pre-#954 sessions, a gate run outside the `run-quality-gate.mjs` wrapper, and the `verification-auto-fix` producer in `scripts/lib/quality-gate.mjs`, which emits `counts` but no `wave_number` (its records are mid-wave retries, so not matching the selector is correct).
187
- > **`suite_platform` has no event source at all** and is read from the STATE.md header, unchanged. **Remaining work to retire the prose path fully:** (1) carry the platform on the gate event payload; (2) once a session has landed with the event path green and no fallback hits, drop the hand-written trio from `wave-loop.md` step 7. Until both hold, the trio stays written — deleting the writer before the reader is proven loses the numbers for sessions in flight.
187
+ > `WAVE` and `SEMANTIC_SESSION_ID` come from the existing own session/wave identity; never construct either for this lookup. Filtering by both is mandatory: the ledger accumulates across sessions, and a different session or wave must not supply these counts. `counts.passed` → `suite_passed`, `counts.failed` → `suite_failed`. `tail -1` selects the last matching gate run of the wave, including auto-fix. `-R` with `fromjson? | objects` skips an unreadable ledger line (a torn tail from a killed writer, #1401) instead of letting one parse error end `jq` and silently empty the selector behind `| tail -1`. The library emits one record per `runQualityGateWithRetry` call, not per retry; `attempts` is not a record count.
188
+ >
189
+ > **Header fallback for counts is limited to three cases:** pre-#954 sessions, a gate run outside the event wrapper, or missing wave attribution (including an auto-fix call without a readable own wave scope). An event without `semantic_session_id` cannot match the selector — emitters set it only from the session lock via `sessionAttribution()` — and is treated like a gate run outside the wrapper. In those cases only, read the STATE.md Wave History header `— suite <passed>/<failed> on <platform>`. An auto-fix call with a known wave uses the event selector above. An empty selector alone is not a fallback case: an event without `counts`, for example after fail-fast before the test suite, supplies no replacement zeros and does not by itself permit header fallback.
190
+ >
191
+ > If no counts are available and none of those fallback cases applies, omit `suite_passed` and `suite_failed`. Absent means "not measured"; a present `suite_failed: 0` means "measured, zero failures". Never zero-fill missing measurements.
192
+ >
193
+ > **`suite_platform` comes separately and exclusively from the STATE.md Wave History header. There is no platform field in the event payload.** Omit it when no header platform measurement is available; do not infer it from `variant`, runner OS, or session platform. Event-derived counts may still be mirrored into the existing header as compatibility output, not a second measurement source; keep the header and sessions.jsonl contracts unchanged.
188
194
  > - `open_questions_asked` / `open_questions_answered` / `open_questions_deferred` (#773): the three open-question counts from the Phase 1.65 gate's AUQ Call 2 (identical to the `questions_*` payload fields on the `orchestrator.handover.gated` event). Top-level, additive, non-negative integers. Populate ONLY when the gate ran an interactive triage ("Closen + Triage" path). OMIT all three (do NOT write `0`) when the gate was skipped (fail-open / headless / disabled) or took the fast-path — absent = "not measured", `0` = "measured, zero questions". Validator accepts absent/null/non-negative-integer.
189
195
  > - `stagnation_events`: populated ONLY when ≥1 stagnation event was logged to `events.jsonl` during this session. When `total == 0`, the field is omitted from the JSONL entry.
190
196
  > - `grounding_injections`: populated ONLY when ≥1 `orchestrator.grounding.injected` event was logged to `events.jsonl` during this session. When `count == 0`, the field is omitted from the JSONL entry.
@@ -27,9 +27,19 @@ if [[ "$VS_ENABLED" == "true" ]]; then
27
27
  done < <(echo "$CONFIG" | jq -r '."vault-sync".exclude // [] | .[]')
28
28
 
29
29
  # Invoke validator; capture JSON on stdout and exit code
30
+ # stderr carries the infra-error envelope on exit 2 (reason + setup hint)
31
+ VS_ERR_FILE=$(mktemp)
30
32
  VS_JSON=$(VAULT_DIR="$VS_DIR" bash "$PLUGIN_ROOT/skills/vault-sync/validator.sh" \
31
- --mode "$VS_MODE" "${VS_EXCLUDE_ARGS[@]}" 2>/dev/null) || VS_EXIT=$?
33
+ --mode "$VS_MODE" "${VS_EXCLUDE_ARGS[@]}" 2>"$VS_ERR_FILE") || VS_EXIT=$?
32
34
  VS_EXIT="${VS_EXIT:-0}"
35
+ if [[ "$VS_EXIT" -eq 2 ]]; then
36
+ VS_LAST=$(tail -n 1 "$VS_ERR_FILE")
37
+ # A plain-text stderr line (e.g. an invalid --mode) is the reason itself.
38
+ VS_REASON=$(jq -r '.reason // empty' <<<"$VS_LAST" 2>/dev/null || true)
39
+ VS_REASON="${VS_REASON:-${VS_LAST:-unknown}}"
40
+ VS_SETUP=$(jq -r '.setup // empty' <<<"$VS_LAST" 2>/dev/null || true)
41
+ fi
42
+ rm -f "$VS_ERR_FILE"
33
43
 
34
44
  VS_STATUS=$(echo "$VS_JSON" | jq -r '.status')
35
45
  VS_ERR_COUNT=$(echo "$VS_JSON" | jq -r '.errors | length')
@@ -42,7 +52,7 @@ fi
42
52
  - **`mode: off`** — validator reports `status: skipped-mode-off`; include a single line "Vault validation: skipped (mode=off)" in the quality gate report and move on. Never blocks.
43
53
  - **`mode: warn`** — validator always exits 0. If `.errors | length > 0`, surface the error list in the report under "Vault validation warnings (mode=warn)" with file + path + message for each entry. Also list any `.warnings` (dangling wiki-links) in the same section. Never blocks close, but remind the user that flipping to `mode: hard` would have blocked on N files.
44
54
  - **`mode: hard`** — validator exits 1 on errors. On exit 1: do NOT block the close. Surface the full error list in the quality gate report, then default to **warn + carryover + continue** (Recommended): file a carryover issue (labels `carryover`, `priority::high`) titled `[Carryover] Vault validation (hard) — <N> frontmatter errors` capturing the offending files for a follow-up session, log a Deviation entry in STATE.md `## Deviations`, then continue the close. Offer "Override and close" (continue without a carryover issue; log the Deviation) as an alternative via AskUserQuestion. The user can also (a) fix the offending frontmatter, (b) add the file pattern to `vault-sync.exclude` if it is a legitimate index file (e.g. `_MOC.md`, `_overview.md`), or (c) temporarily set `vault-sync.mode: warn` while backfilling frontmatter across the vault. On exit 0 with warnings: include them in the report but do not block.
45
- - **Exit 2** (infra error — missing `node`, `pnpm`, or `validator.mjs`) — treat as a skipped gate with a loud warning ("Vault validation: infrastructure error — <reason>"). Do NOT block the session close on infra failures; the goal is to surface configuration problems, not to wedge sessions when Node is unavailable.
55
+ - **Exit 2** (infra error — missing `node` or `validator.mjs`, or `setup-required` when `zod`/`yaml` do not resolve) — treat as a skipped gate with a loud warning ("Vault validation: infrastructure error — $VS_REASON"), followed by `$VS_SETUP` when it is set. Do NOT block the session close on infra failures; the goal is to surface configuration problems, not to wedge sessions when Node is unavailable.
46
56
 
47
57
  **Success line format** (when `errors: [] && warnings: []`):
48
58
  ```
@@ -326,7 +326,7 @@ Operator-selected issues (from Phase 6) are appended AFTER this loop, not interl
326
326
 
327
327
  ## Phase 7.1: Issue Premise Verification (#730/H3)
328
328
 
329
- > Skip for `housekeeping` sessions (Phase 7 already skips deep research there).
329
+ > Housekeeping too (#1521).
330
330
  > Runs on the shortlisted candidate issues from Phase 6 Pattern Recognition
331
331
  > (cap: 8 issues — cost control; prioritize the issues most likely to enter scope).
332
332
 
@@ -354,7 +354,7 @@ Invokes `buildLiveSignals` (single SSOT for the signals shape) then `selectMode(
354
354
 
355
355
  > Skip silently when `persistence: false` (no ledger for the event). Runs BEFORE the first writing action; everything not stated here is in `skills/_shared/fleet-protocol.md`.
356
356
 
357
- Build the check-in JSON (fields per the reference; `session` from `.orchestrator/current-session.json`, `modus` = session type, `kandidaten` = the issue numbers you intend to claim) and pipe it into `node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/fleet-checkin.mjs"` — it validates, stores `checkin/<session_id>.json` (0600) under `~/.config/navigator/` and emits `orchestrator.fleet.checkin`. Its stdout carries `navigator_state` and `navigator_adresse` (the lease's validated `adresse`, else `null`): on `active`, and only when `navigator_adresse` is non-null and reachable via `ListAgents`, send ONE hint to exactly that name, naming the file path — never expect or wait for a reply (CSM-004). Then read `auflagen/<session_id>.json`; if it is still absent at the stdout's `fallback_due_at`, the reference's Standard-Auflagen apply (the CLI computes that deadline: `zeit` plus `CHECKIN_FALLBACK_MIN` while a navigator is active, `zeit` itself — immediately — otherwise). Reading work and edits in the own worktree continue meanwhile.
357
+ Build the check-in JSON (fields per the reference; `session` from `.orchestrator/current-session.json`, `modus` = session type, `kandidaten` = the issue numbers you intend to claim) and pipe it into `node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/fleet-checkin.mjs"` — it validates, stores `checkin/<session_id>.json` (0600) under `~/.config/navigator/` and emits `orchestrator.fleet.checkin`. Its stdout carries `navigator_state` and `navigator_adresse` (the lease's validated `adresse`, else `null`): on `active`, and only when `navigator_adresse` is non-null and reachable via `ListAgents`, send ONE hint to exactly that name, naming the file path — never expect or wait for a reply (CSM-004). Then read `auflagen/<session_id>.json` via `node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/fleet-auflagen.mjs" <session_id>` (only a file whose `repo` resolves to this repo's git common dir is `accepted`; a `rejected` one counts as absent); if it is still absent at the stdout's `fallback_due_at`, the reference's Standard-Auflagen apply (the CLI computes that deadline: `zeit` plus `CHECKIN_FALLBACK_MIN` while a navigator is active, `zeit` itself — immediately — otherwise). Reading work and edits in the own worktree continue meanwhile.
358
358
 
359
359
  ## Phase 8: Structured Presentation & Q&A
360
360
 
@@ -18,6 +18,7 @@ For each claim, run exactly ONE Grep or Read against the actual codebase (or
18
18
  transcript discipline as `.claude/rules/parallel-sessions.md` § PSA-006.
19
19
  Budget: this phase is a spot-check, not a deep audit. If one grep cannot
20
20
  ground the claim, mark UNVERIFIED and move on — do not rabbit-hole.
21
+ Per issue, also run `git log --grep='#<N>'`: fixes often land without closing the issue.
21
22
 
22
23
  ## Verdicts
23
24
 
@@ -44,4 +45,4 @@ decomposing) without re-running the greps.
44
45
 
45
46
  - Re-verifying every sentence of every issue — cap 3 claims/issue, 8 issues.
46
47
  - Treating UNVERIFIED as a blocker — it is informational only.
47
- - Running this for housekeeping sessions — predefined tasks carry no premise risk worth the latency.
48
+ - Skipping it in housekeeping — items go stale there too (#1521: 4 of 7 were fixed).
@@ -50,7 +50,10 @@ workflow with its own verified scope, tests and review; it does not turn this en
50
50
  into a fabricated wave session.
51
51
  Before the first writing action, write the fleet check-in and read the fleet's constraints —
52
52
  the same step as session-start Phase 7.6 (`scripts/fleet-checkin.mjs`; fields and the
53
- Standard-Auflagen in `skills/_shared/fleet-protocol.md`). If no `auflagen/` file has appeared by
53
+ Standard-Auflagen in `skills/_shared/fleet-protocol.md`). Read the Auflagen with
54
+ `node scripts/fleet-auflagen.mjs <session_id>`, never the file directly: only a file whose `repo`
55
+ resolves to this repo's git common dir is `accepted`, and a `rejected` one counts as absent
56
+ (`skills/_shared/fleet-protocol.md` § `auflagen/<session_id>.json`). If no `auflagen/` file has appeared by
54
57
  the `fallback_due_at` on the CLI's stdout, the Standard-Auflagen apply: the CLI sets it
55
58
  `CHECKIN_FALLBACK_MIN` after `zeit` while a navigator is active, and to `zeit` itself —
56
59
  immediately — otherwise. A hint to the navigator is optional and never awaited.
@@ -6,6 +6,7 @@ description: >
6
6
  layout. Read-only side-channel observability — the coordinator chat stays in the operator's original
7
7
  terminal. Trigger phrases: "tmux layout", "split panes for ci watch", "visualize session side-channels",
8
8
  "show me state-md tail and ci".
9
+ user-invocable: true
9
10
  model: inherit
10
11
  color: cyan
11
12
  tools: Read, Bash, Grep, Glob
@@ -10,6 +10,7 @@ description: >
10
10
  notes to vault", "vault-mirror failed at session close". <example>Context: session-end is finalizing,
11
11
  vault-integration.mode is "warn". user: "/close" assistant: "Running vault-mirror to write
12
12
  50-sessions/session-2026-05-17.md from the closing session record — 1 created, 0 skipped."</example>
13
+ user-invocable: false
13
14
  model: haiku
14
15
  ---
15
16
 
@@ -10,6 +10,7 @@ description: >
10
10
  vault-sync". <example>Context: session-end Phase 1 quality gate, vault-sync.enabled=true,
11
11
  vault-sync.mode="hard". user: "/close" assistant: "vault-sync found 2 frontmatter errors in
12
12
  vault/40-learnings/ml-notes.md — missing required `id` field. Fixing before close."</example>
13
+ user-invocable: false
13
14
  model: haiku
14
15
  ---
15
16
 
@@ -28,9 +29,8 @@ Phase 1 ships a self-contained validator that reads every `.md` file under `VAUL
28
29
  ### Files
29
30
 
30
31
  - `validator.mjs` — Node.js ESM validator. Uses `zod` + `yaml` npm packages. Reads `VAULT_DIR` (env or default cwd), walks the tree, skipping `node_modules/`, `.git/`, `.obsidian/`. `90-archive/` is walked but never *checked* (#833): archived notes stay in the link-target register — so an inbound `[[wiki-link]]` to an archived note resolves instead of dangling — while their frontmatter is skipped and counted in `archived_skipped_count`. For each `.md`: parses frontmatter, validates against the inline Zod schema, extracts `[[wiki-links]]`, verifies each target resolves. Emits JSON report on stdout.
31
- - `validator.sh` — Thin POSIX wrapper. Resolves `VAULT_DIR` from arg 1 or env, self-bootstraps deps via `pnpm install --silent` on first run, execs the Node validator. Session-end and other callers use this entry point.
32
- - `package.json` — Declares `zod` (`^3.24.0`, matching projects-baseline) and `yaml` (`^2.5.0`) as deps. `pnpm-lock.yaml` is committed; `node_modules/` is gitignored.
33
- - `tests/validator.bats` — 16 BATS cases covering clean vaults, broken frontmatter, missing required fields, dangling links, no-vault skipping, README-style files, nested directories, and archive/obsidian exclusion.
32
+ - `validator.sh` — Thin POSIX wrapper. Resolves `VAULT_DIR` from arg 1 or env, checks that `zod` and `yaml` resolve from the skill directory, execs the Node validator. It never installs anything: unresolvable deps exit 2 with `"reason":"setup-required"` and the one setup command, `npm ci` in the plugin root (#1070). Session-end and other callers use this entry point.
33
+ - `package.json` — Declares `zod` (`^3.24.0`, matching projects-baseline) and `yaml` (`^2.5.0`) as deps. No lockfile is committed (`skills/vault-sync/package-lock.json` is gitignored at the repo root); `node_modules/` is gitignored.
34
34
  - `tests/fixtures/` — Seven fixture vaults matching each test scenario.
35
35
 
36
36
  ### Schema source
@@ -41,9 +41,9 @@ The inline Zod schema is vendored from the canonical source at `projects-baselin
41
41
 
42
42
  `skills/vault-sync/package.json` pins `yaml ^2.5.0` / `zod ^3.24.0` — intentionally NOT the root's `yaml ^2.9.0` / `zod ^3.25.76`. This is not drift to fix:
43
43
 
44
- - Both CI hosts install this sub-package on its own, independent of the root install (`.gitlab-ci.yml:121` and `.github/workflows/test.yml:93`, both: `(cd skills/vault-sync && npm install --no-audit --no-fund)`).
45
- - The root `package.json` declares no `workspaces`, so `npm ci` at the root never touches this folder's deps; `skills/vault-sync/node_modules/` is the only place `zod` resolves for this skill (`scripts/lib/vault-archive.mjs:19-21`).
46
- - The `zod ^3.24.0` pin deliberately tracks the projects-baseline version, not this repo's own (`scripts/release.mjs:488`).
44
+ - Nothing installs this sub-package. Neither CI host runs a nested install any more — both run only the root `npm ci`, and `.husky/pre-push` links only the root `node_modules/` into its tracked-tree clone. The nested install was dropped on 2026-10-05: it pulled unlocked `zod`/`yaml` versions (this folder's lockfile is gitignored) that the root `npm audit` never saw, and the vault-sync test files ran 125 passed / 0 failed without it.
45
+ - The root `package.json` declares no `workspaces`, so `npm ci` at the root never touches this folder's deps. Node resolves `zod`/`yaml` from this folder via the root's own `dependencies` (`yaml ^2.9.0`, `zod ^3.25.76`) — measured 2026-10-05 via `import.meta.resolve('zod')` from this folder → `<root>/node_modules/zod`. That is the runtime path in CI, in every worktree and for every end user, so the root dependencies must stay. A stale local `skills/vault-sync/node_modules/` from an older manual install would still win resolution; delete it if versions disagree.
46
+ - The `zod ^3.24.0` pin deliberately tracks the projects-baseline version, not this repo's own (`scripts/release.mjs:622`).
47
47
 
48
48
  The root's dependency versions are NOT the SSOT for this folder — do not "fix" this pin to match the root.
49
49
 
@@ -8,13 +8,18 @@
8
8
  # Behavior:
9
9
  # - Resolves VAULT_DIR from arg 1 or env.
10
10
  # - Ensures `node` is available.
11
- # - Auto-installs deps on first run (pnpm install --silent) if node_modules missing.
11
+ # - Checks that `zod` and `yaml` resolve from this directory. They are
12
+ # runtime dependencies of the ROOT package (package.json `dependencies`),
13
+ # so Node resolves them from the plugin root's node_modules/. Missing →
14
+ # exit 2 with status `setup-required` and the one npm-canonical setup
15
+ # command. Never installs anything itself (#1070 AC3/AC6).
12
16
  # - Execs validator.mjs; propagates exit code.
13
17
  #
14
18
  # Exit codes (mirror validator.mjs):
15
19
  # 0 — vault valid (or skipped: no vault)
16
20
  # 1 — validation errors
17
- # 2 — infrastructure error (missing node / pnpm, missing validator.mjs)
21
+ # 2 — infrastructure error (missing node, missing validator.mjs,
22
+ # dependencies not installed → status `setup-required`)
18
23
  #
19
24
  # Output: JSON report on stdout (machine-readable).
20
25
 
@@ -36,18 +41,32 @@ if ! command -v node >/dev/null 2>&1; then
36
41
  exit 2
37
42
  fi
38
43
 
44
+ # Emit the infra-error envelope via node so a path containing a quote or a
45
+ # backslash still yields valid JSON (paths travel through env, never through
46
+ # the JSON text). Args: reason [detail] [setup].
47
+ infra_error() {
48
+ SO_VS_REASON="$1" SO_VS_DETAIL="${2:-}" SO_VS_SETUP="${3:-}" node -e '
49
+ const o = { status: "infra-error", reason: process.env.SO_VS_REASON };
50
+ if (process.env.SO_VS_DETAIL) o.detail = process.env.SO_VS_DETAIL;
51
+ if (process.env.SO_VS_SETUP) o.setup = process.env.SO_VS_SETUP;
52
+ console.error(JSON.stringify(o));' >&2
53
+ }
54
+
39
55
  if [[ ! -f "$VALIDATOR_MJS" ]]; then
40
- echo "{\"status\":\"infra-error\",\"reason\":\"validator.mjs not found at $VALIDATOR_MJS\"}" >&2
56
+ infra_error "validator.mjs not found at $VALIDATOR_MJS"
41
57
  exit 2
42
58
  fi
43
59
 
44
- # ── Self-bootstrap node_modules on first run ───────────────────────────────
45
- if [[ ! -d "$SCRIPT_DIR/node_modules" ]]; then
46
- if ! command -v pnpm >/dev/null 2>&1; then
47
- echo '{"status":"infra-error","reason":"pnpm not found; cannot bootstrap dependencies"}' >&2
48
- exit 2
49
- fi
50
- (cd "$SCRIPT_DIR" && pnpm install --silent) >&2
60
+ # ── Dependency readiness (no install, no foreign package manager) ─────────
61
+ # `zod`/`yaml` come from the root package's `dependencies`; a nested
62
+ # skills/vault-sync/node_modules/ is NOT required. Probe resolution from this
63
+ # directory instead of testing for a particular node_modules folder, so an npm
64
+ # install (deps hoisted above the package) and a plugin checkout both pass.
65
+ if ! (cd "$SCRIPT_DIR" && node --input-type=module \
66
+ -e "import.meta.resolve('zod'); import.meta.resolve('yaml');") >/dev/null 2>&1; then
67
+ PLUGIN_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
68
+ infra_error "setup-required" "zod/yaml not resolvable from $SCRIPT_DIR" "cd $(printf %q "$PLUGIN_ROOT") && npm ci"
69
+ exit 2
51
70
  fi
52
71
 
53
72
  # ── Execute validator ──────────────────────────────────────────────────────
@@ -84,7 +84,7 @@ const gate = await evaluateWaveResourceGate({
84
84
 
85
85
  **The offload witness is supplied, never probed (#1160).** The gate's own JSDoc states it: `@param {Record<string, boolean>} [opts.remoteReady] — #1160 readiness witness per declared host alias. The gate never probes the network itself; without a witness no host counts as ready and the decision stays local.` So an `offload` decision is impossible unless the coordinator passes one of two things alongside `config`/`plannedAgents`/`waveRole`:
86
86
 
87
- - `remoteReady: { '<alias>': true }` — built from the SessionStart banner line `Offload <alias>: ready=yes`, which was measured at session start and needs no new network call here; or
87
+ - `remoteReady: { '<alias>': true }` — built from the SessionStart banner line `Offload <alias>: headless-ready=yes` (legacy `ready=yes` only without new readiness markers; `gate-ready` does not authorize Claude dispatch), which was measured at session start and needs no new network call here; or
88
88
  - `probeFn: remoteReadyProbe` from `scripts/lib/wave-executor/remote-dispatch.mjs` — `@param {(alias: string) => Promise<boolean>} [opts.probeFn] — optional async witness, consulted only for aliases absent from `remoteReady`. Default null.` Use it when the banner is stale or absent; it costs a live probe per alias.
89
89
 
90
90
  Passing neither is a valid choice, not a bug: the gate then behaves exactly as it did before #1160 and reduces locally.
@@ -378,15 +378,21 @@ If the commit itself fails (e.g., nothing to commit, pre-commit hook rejects), d
378
378
  **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:
379
379
 
380
380
  ```bash
381
- jq -c --argjson w <wave_number> --arg s "<semantic_session_id>" '
382
- select(.event | startswith("orchestrator.quality_gate."))
383
- | select(.semantic_session_id == $s and .wave_number == $w and .counts != null)
384
- | .counts' .orchestrator/metrics/events.jsonl | tail -1
381
+ jq -cR --argjson w "$WAVE" --arg s "$SEMANTIC_SESSION_ID" '
382
+ fromjson? | objects
383
+ | select((.event | type) == "string" and (.event | startswith("orchestrator.quality_gate.")))
384
+ | select(.semantic_session_id == $s and .wave_number == $w and .["counts"] != null)
385
+ | .["counts"]
386
+ ' .orchestrator/metrics/events.jsonl | tail -1
385
387
  ```
386
388
 
387
- The session filter is not optional — `events.jsonl` accumulates across sessions and every past session also had a wave with this number.
388
- **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.
389
- > **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.
389
+ `WAVE` and `SEMANTIC_SESSION_ID` come from the existing own session/wave identity; never construct either for this lookup. Filtering by both is mandatory: the ledger accumulates across sessions, and a different session or wave must not supply these counts. `counts.passed` → `suite_passed`, `counts.failed` → `suite_failed`. `tail -1` selects the last matching gate run of the wave, including auto-fix. `-R` with `fromjson? | objects` skips an unreadable ledger line (a torn tail from a killed writer, #1401) instead of letting one parse error end `jq` and silently empty the selector behind `| tail -1`. The library emits one record per `runQualityGateWithRetry` call, not per retry; `attempts` is not a record count.
390
+
391
+ **Fallback for counts is limited to three cases:** pre-#954 sessions, a gate run outside the event wrapper, or missing wave attribution (including an auto-fix call without a readable own wave scope). An event without `semantic_session_id` cannot match the selector — emitters set it only from the session lock via `sessionAttribution()` — and is treated like a gate run outside the wrapper. In those cases only, fall back to the gate output you read for this wave — the same run whose counts go into the § 3a header — and say so in the progress update. Do not read the STATE.md Wave History header here: this step runs before § 3a writes this wave's header, so the newest header line there belongs to the previous wave. An auto-fix call with a known wave uses the event selector above. An empty selector alone is not a fallback case: an event without `counts`, for example after fail-fast before the test suite, supplies no replacement zeros and does not by itself permit header fallback.
392
+
393
+ If no counts are available and none of those fallback cases applies, omit `suite_passed` and `suite_failed`. Absent means "not measured"; a present `suite_failed: 0` means "measured, zero failures". Never zero-fill missing measurements.
394
+
395
+ **`suite_platform` comes separately from that same gate run of this wave — the platform it ran on, which § 3a then writes as `on <platform>` into the header. There is no platform field in the event payload.** Omit it when the platform of that run is not known; never infer it from `variant`. Event-derived counts may still be mirrored into the existing header as compatibility output, not a second measurement source; keep the header and sessions.jsonl contracts unchanged.
390
396
  Append this wave record to the session metrics `waves` array.
391
397
 
392
398
  7a. **Scope drift tripwire (S2 — #896, warn-only)**: session-cumulative and filtered through `DRIFT_EXCLUDE_PATTERNS`, so its `filesRatio` is NOT expected to agree with this wave's unfiltered `files_changed` from step 7. Only the committed part of its numerator starts at `session-start-ref`; the uncommitted and untracked parts read the checkout as it is now, so files already dirty at session start and a same-checkout peer's files count too (an over-report, named as a ceiling in `computeDrift()`'s JSDoc). Call `computeDrift()` from the same `scripts/lib/scope-baseline.mjs` module as `wave-loop-dispatch.md` § 0a Scope Baseline Freeze. Never blocks — exit code stays 0 and the next wave is dispatched regardless of the result.
@@ -87,7 +87,7 @@ Before each wave dispatch:
87
87
 
88
88
  The per-agent path IS `$AGENT_FILESCOPE_JSON` — the same file `--assert-subset` (#796 below), Grounding Injection (#85), the Learnings-Index (#1014) and File-Scope Injection (#1020) consume. Never write a `$TMPDIR` copy: it degrades to a signal-free allow when an injector cannot find the addressable wave-keyed file. The coordinator's record is materialized as `coordinator.json` and included in the aggregate, so its direct edits are covered by the two checks below.
89
89
 
90
- > **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs before the union exists, so writing these files reports `bash-write-verify: N file(s) changed by a Bash call OUTSIDE the wave's allowedPaths` naming `filescopes/wave-<N>/*.json`. Expected once per wave rollover at this step; it is information, not a scope violation. Never widen `allowedPaths` to silence it — that would grant agents write access to the deconfliction record itself.
90
+ > **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs before the union exists, so `hooks/post-bash-write-verify.mjs` drops these files from its out-of-scope report the same way it drops `wave-scope.json` — keyed on the active manifest's state dir (`isScopeDeclarationPath`, #1027 Pkt 5) — and writing them reports nothing. Never widen `allowedPaths` instead: that would grant agents write access to the deconfliction record itself.
91
91
 
92
92
  **3.1a — a peer session's declared paths are their OWN record (#1195).** When a reachable peer session runs in this same checkout and has sent a Peer-Scope-Union request (`skills/_shared/parallel-aware-auq.md` § Peer-Scope-Union), carry its complete path list into the record array as exactly ONE record `{"id":"peer-session-<id>","files":[…]}` — `<id>` being the peer's semantic session id. Never merge peer paths into an agent's record: an agent record is a territory ONE agent may write, and a peer's paths are a territory NO agent of this wave may write. Keeping them separate is also what makes the disjointness check meaningful — a peer path colliding with an agent's scope is a real collision that must surface at 3.2, not be laundered by living in that agent's own record. Mechanically, a peer record takes part in `--assert-disjoint` and is EXCLUDED from `--union` (`unionFileScopes`, `scripts/lib/scope-gate.mjs`), so `allowedPaths` never grants a peer's paths to this wave's agents — which is precisely what keeps them a territory no agent may write. It also gets NO per-agent shape-(a) file: `materialize-wave-scope.mjs` writes peer records into the aggregate only, because `$AGENT_FILESCOPE_JSON` is addressed by an agent id at dispatch and no agent is dispatched for a peer session. `hooks/post-bash-write-verify.mjs` reads the same aggregate sidecar and reports a change under a `peer-session-*` record as a peer write instead of a violation — reachable only BECAUSE the union excludes it (the hook sees a path only while it is outside `allowedPaths`).
93
93
 
@@ -121,11 +121,11 @@ Before each wave dispatch:
121
121
 
122
122
  > **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.
123
123
 
124
- **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.
124
+ **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. The measurement is `scripts/lib/test-sibling-coverage.mjs`, not a count restated here — run `node scripts/lib/test-sibling-coverage.mjs --json` and cite its envelope (it carries its own definition, `ref` and the uncovered list). It counts production files by negation (every tracked file a sibling-rule source matches that is not test-side) and reports three figures: **glob** (the scope gate's own `testSiblingsFor()` + `pathMatchesPattern()`), **exact** (a `tests/**/<stem>.test.mjs` exists), **mirror** (the naive 1:1 path `tests/<path minus scripts/>.test.mjs`). Measured at `HEAD=af58a173`, 2026-10-03, index clean, over the script's population (tracked `.mjs` matched by the sibling-rule source minus every test-side path, 571 files): glob **511/571 (89.5 %)**, exact **491/571 (86.0 %)**, mirror **353/571 (61.8 %)**. So the glob is right ~9 times in 10 and *harmless* when wrong — it grants write access to a path that may not exist; a computed mirror path would be wrong ~38 % of the time **and still deny the real test**. The residual is real, mostly semantic naming (`scripts/lib/learnings/*.mjs` → `tests/unit/learnings.test.mjs`) and untested root config files: 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 a default, not a guarantee.
125
125
 
126
- > 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).
126
+ > Re-measure before citing these figures downstream — a count re-briefed later is a claim about the past (`.claude/rules/parallel-sessions.md` § PSA-006). The previous hand count (`730ee9d`, 2026-08-03: 375 / 272 of 439, denominator `scripts/` + `hooks/` + `skills/` only) is superseded by the script (#1030 P3).
127
127
 
128
- > **Over-grant ceiling (#1030):** the glob is a basename *prefix* (`tests/**/{basename}*.test.mjs`, `scripts/lib/scope-gate.mjs:312`), so a production file sharing a basename with another receives the other's test siblings — and a stem that prefixes another (`cli` → `cli-flags`) does too, so the figure below is a lower bound. Measured at `HEAD=0e0c4b08`, 2026-10-03: `git ls-files '*.mjs' | grep -v '^tests/' | xargs -n1 basename | sort | uniq -d | wc -l` → **34** colliding basenames, covering **80 of 572** production `.mjs` (**14.0 %**; files via `… | sort | uniq -D | wc -l`, denominator via `git ls-files '*.mjs' | grep -v '^tests/' | wc -l`; top: `schema.mjs` 8×). The error direction is the harmless one — a too-wide grant, never a denied write. **Revisit-Trigger:** a plan author disables the sibling expansion to dodge a false-positive collision between two same-named production files — then scope the glob by directory, but NOT by a raw `{dir}`: `renderSiblingTemplate` fills it with `scripts/lib`, while the test tree drops the `scripts/` prefix (`tests/lib/`), so a raw `{dir}` would deny real test writes; it needs a source→test prefix mapping first.
128
+ > **Over-grant ceiling (#1030):** the glob is a basename *prefix* (`tests/**/{basename}*.test.mjs`, `scripts/lib/scope-gate.mjs:312`), so a production file sharing a basename with another receives the other's test siblings — and a stem that prefixes another (`cli` → `cli-flags`) does too, so the figure below is a lower bound. Measured at `HEAD=0e0c4b08`, 2026-10-03: `git ls-files '*.mjs' | grep -v '^tests/' | xargs -n1 basename | sort | uniq -d | wc -l` → **34** colliding basenames, covering **80 of 572** production `.mjs` (**14.0 %**; population = tracked `.mjs` outside `tests/` at `0e0c4b08`, which still counts the one co-located test `skills/vault-sync/tests/schema-drift.test.mjs` — the whole gap to the script's 571, the same recipe also gives 572 at `af58a173`; files via `… | sort | uniq -D | wc -l`, denominator via `git ls-files '*.mjs' | grep -v '^tests/' | wc -l`; top: `schema.mjs` 8×). The error direction is the harmless one — a too-wide grant, never a denied write. **Revisit-Trigger:** a plan author disables the sibling expansion to dodge a false-positive collision between two same-named production files — then scope the glob by directory, but NOT by a raw `{dir}`: `renderSiblingTemplate` fills it with `scripts/lib`, while the test tree drops the `scripts/` prefix (`tests/lib/`), so a raw `{dir}` would deny real test writes; it needs a source→test prefix mapping first.
129
129
 
130
130
  **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.
131
131
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: write-executable-plan
3
3
  description: Use when you have a PRD or design spec and need a bite-sized, executable implementation plan that any agent can follow without re-deriving structure. Produces `docs/plans/YYYY-MM-DD-<feature>.md` with per-task Files block, complete code per step (no placeholders), and exact verification commands. Rejects "TBD", "TODO", "add error handling", "similar to Task N".
4
+ user-invocable: true
4
5
  model: inherit
5
6
  color: green
6
7
  tools: Read, Grep, Glob, Bash, Write