session-orchestrator 5.3.0 → 5.5.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 (260) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/test-audit/SKILL.md +21 -0
  3. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  8. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  9. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  10. package/.cursor/commands/test-audit.md +14 -0
  11. package/.cursor/rules/060-evolve.mdc +28 -13
  12. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  13. package/.cursor/skills/test-audit/SKILL.md +12 -0
  14. package/.cursor-plugin/plugin.json +1 -1
  15. package/.orchestrator/policy/blocked-commands.json +1 -1
  16. package/AGENTS.md +9 -31
  17. package/CHANGELOG.md +162 -0
  18. package/README.md +14 -10
  19. package/docs/agent-authoring.md +2 -2
  20. package/docs/baseline.md +10 -1
  21. package/docs/ci-setup.md +154 -4
  22. package/docs/components.md +11 -11
  23. package/docs/events-schema.md +25 -19
  24. package/docs/install.md +14 -2
  25. package/docs/migration-v4.md +24 -17
  26. package/docs/persona-panel.md +5 -4
  27. package/docs/plugin-architecture-v3.md +1 -1
  28. package/docs/rule-authoring.md +27 -12
  29. package/docs/scope-collision-guard.md +40 -16
  30. package/docs/session-config-reference.md +61 -12
  31. package/docs/session-config-template.md +11 -2
  32. package/docs/vault-docs-architecture.md +8 -4
  33. package/hooks/_lib/hook-import-set.json +42 -29
  34. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  35. package/hooks/config-protection.mjs +1 -1
  36. package/hooks/enforce-commands.mjs +28 -13
  37. package/hooks/enforce-scope.mjs +201 -23
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks-cursor.json +5 -0
  40. package/hooks/hooks-pi.json +5 -0
  41. package/hooks/hooks.json +6 -1
  42. package/hooks/on-session-start.mjs +57 -2
  43. package/hooks/on-stop.mjs +70 -168
  44. package/hooks/post-bash-write-verify.mjs +98 -17
  45. package/hooks/post-edit-validate.mjs +15 -5
  46. package/hooks/post-tool-batch-wave-signal.mjs +32 -152
  47. package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
  48. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  49. package/hooks/pre-bash-staging-fence.mjs +29 -8
  50. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  51. package/hooks/pre-task-scope-disjoint.mjs +1143 -279
  52. package/hooks/subagent-telemetry.mjs +96 -19
  53. package/hooks/wave-scope-commit-guard.mjs +168 -72
  54. package/package.json +2 -6
  55. package/pi/prompts/test-audit.md +12 -0
  56. package/rules/README.md +6 -1
  57. package/rules/_index.md +3 -3
  58. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  59. package/rules/opt-in-domain/prompt-caching.md +5 -1
  60. package/rules/opt-in-stack/backend-data.md +9 -1
  61. package/rules/opt-in-stack/backend.md +7 -1
  62. package/rules/opt-in-stack/frontend.md +6 -1
  63. package/rules/opt-in-stack/security-web.md +7 -1
  64. package/rules/opt-in-stack/swift.md +5 -1
  65. package/scripts/apply-session-learnings.mjs +513 -0
  66. package/scripts/archive-closed-prds.mjs +40 -12
  67. package/scripts/autopilot.mjs +57 -54
  68. package/scripts/backfill-abandoned-sessions.mjs +81 -6
  69. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  70. package/scripts/baseline-archetypes.mjs +1 -1
  71. package/scripts/check-package-manager.mjs +62 -5
  72. package/scripts/codex-install.mjs +3 -0
  73. package/scripts/cursor-install.mjs +47 -6
  74. package/scripts/emit-session.mjs +298 -2
  75. package/scripts/export-hw-learnings.mjs +40 -10
  76. package/scripts/fleet-checkin.mjs +170 -0
  77. package/scripts/lib/agent-frontmatter.mjs +3 -3
  78. package/scripts/lib/auto-dream.mjs +7 -6
  79. package/scripts/lib/autopilot/flags.mjs +6 -1
  80. package/scripts/lib/autopilot/loop.mjs +32 -13
  81. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  82. package/scripts/lib/baseline-archetypes.mjs +41 -22
  83. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  84. package/scripts/lib/common.mjs +41 -0
  85. package/scripts/lib/config/host-paths.mjs +79 -0
  86. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  87. package/scripts/lib/config/reaper.mjs +7 -6
  88. package/scripts/lib/config/vault-integration.mjs +151 -16
  89. package/scripts/lib/config-schema.mjs +13 -0
  90. package/scripts/lib/config.mjs +75 -10
  91. package/scripts/lib/eval/engine.mjs +13 -0
  92. package/scripts/lib/events-retention-banner.mjs +393 -0
  93. package/scripts/lib/events-rotation.mjs +21 -6
  94. package/scripts/lib/events.mjs +72 -5
  95. package/scripts/lib/fleet-protocol.mjs +288 -0
  96. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  97. package/scripts/lib/gates/gate-full.mjs +7 -2
  98. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  99. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  100. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  101. package/scripts/lib/hardening.mjs +1 -0
  102. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  103. package/scripts/lib/instruction-budget-guard.mjs +77 -11
  104. package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
  105. package/scripts/lib/learnings/io.mjs +274 -37
  106. package/scripts/lib/learnings/schema.mjs +163 -15
  107. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  108. package/scripts/lib/lock-reaper.mjs +38 -14
  109. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  110. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  111. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  112. package/scripts/lib/named-vault-resolver.mjs +147 -5
  113. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  114. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  115. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  116. package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
  117. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  118. package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +510 -0
  120. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  121. package/scripts/lib/orphan-reaper.mjs +47 -1315
  122. package/scripts/lib/owner-yaml.mjs +104 -6
  123. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  124. package/scripts/lib/platform.mjs +206 -2
  125. package/scripts/lib/plugin-update-banner.mjs +3 -1
  126. package/scripts/lib/process-group.mjs +134 -22
  127. package/scripts/lib/project-hygiene.mjs +21 -1
  128. package/scripts/lib/quality-gate.mjs +45 -5
  129. package/scripts/lib/resource-probe.mjs +1 -1
  130. package/scripts/lib/rule-loader.mjs +100 -62
  131. package/scripts/lib/rules-sync.mjs +27 -12
  132. package/scripts/lib/scope-baseline.mjs +119 -29
  133. package/scripts/lib/scope-echo.mjs +7 -0
  134. package/scripts/lib/scope-gate.mjs +194 -27
  135. package/scripts/lib/session-close-backfill.mjs +129 -4
  136. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  137. package/scripts/lib/session-lock-shape.mjs +6 -3
  138. package/scripts/lib/session-lock.mjs +320 -67
  139. package/scripts/lib/session-record-repair.mjs +183 -26
  140. package/scripts/lib/session-registry.mjs +67 -17
  141. package/scripts/lib/session-schema/constants.mjs +30 -4
  142. package/scripts/lib/session-schema/validator.mjs +28 -1
  143. package/scripts/lib/session-start-probes.mjs +242 -68
  144. package/scripts/lib/session-token-rollup.mjs +252 -54
  145. package/scripts/lib/sessions-canonical.mjs +100 -9
  146. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  147. package/scripts/lib/sh-quote.mjs +29 -0
  148. package/scripts/lib/subagents-schema.mjs +30 -3
  149. package/scripts/lib/tail-window.mjs +27 -3
  150. package/scripts/lib/telemetry/pricing.mjs +121 -71
  151. package/scripts/lib/telemetry/sync.mjs +17 -0
  152. package/scripts/lib/tests-src-ratio.mjs +215 -23
  153. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  154. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  155. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  156. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  157. package/scripts/lib/validate/check-agents.mjs +1 -1
  158. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  159. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  160. package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
  161. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  162. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  163. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  164. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  165. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  166. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  167. package/scripts/lib/vault-mirror/process.mjs +350 -62
  168. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  169. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  170. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  171. package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
  172. package/scripts/lib/vault-status/board-writer.mjs +53 -12
  173. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  174. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  175. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  176. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  177. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  178. package/scripts/lib/wave-resource-gate.mjs +34 -8
  179. package/scripts/materialize-wave-scope.mjs +7 -4
  180. package/scripts/memory-propose.mjs +18 -5
  181. package/scripts/release-session-lock.mjs +2 -1
  182. package/scripts/release.mjs +11 -3
  183. package/scripts/repair-invalid-sessions.mjs +37 -4
  184. package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
  185. package/scripts/run-quality-gate.mjs +331 -307
  186. package/scripts/self-update.mjs +229 -0
  187. package/scripts/site-numbers.mjs +163 -5
  188. package/scripts/sweep-expired-learnings.mjs +603 -20
  189. package/scripts/sweep-expired-rules.mjs +2 -1
  190. package/scripts/validate-plugin.mjs +7 -0
  191. package/scripts/validate-wave-scope.mjs +21 -2
  192. package/scripts/vault-backfill.mjs +50 -37
  193. package/scripts/vault-integration-watcher.mjs +2 -1
  194. package/scripts/vault-mirror.mjs +238 -75
  195. package/scripts/wave-scope-binding.mjs +2 -1
  196. package/skills/_shared/config-reading.md +3 -3
  197. package/skills/_shared/fleet-protocol.md +134 -0
  198. package/skills/_shared/state-ownership.md +41 -12
  199. package/skills/autopilot/SKILL.md +25 -11
  200. package/skills/bootstrap/SKILL.md +6 -4
  201. package/skills/bootstrap/_shared-template.md +97 -2
  202. package/skills/bootstrap/fast-template.md +4 -1
  203. package/skills/bootstrap/private-contract.md +2 -1
  204. package/skills/bootstrap/public-fallback.md +6 -3
  205. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  206. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  207. package/skills/bootstrap/standard-template.md +2 -0
  208. package/skills/claude-md-drift-check/SKILL.md +10 -10
  209. package/skills/claude-md-drift-check/checker.mjs +26 -18
  210. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  211. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  212. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  213. package/skills/docs-orchestrator/SKILL.md +12 -5
  214. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  215. package/skills/evolve/SKILL.md +13 -4
  216. package/skills/evolve/references/evolve-analyze-mode.md +42 -18
  217. package/skills/memory-cleanup/SKILL.md +1 -1
  218. package/skills/persona-panel/SKILL.md +2 -2
  219. package/skills/persona-panel/persona-format.md +2 -2
  220. package/skills/persona-panel/presets/designer-lens.md +1 -1
  221. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  222. package/skills/persona-panel/presets/pm-lens.md +1 -1
  223. package/skills/plan/mode-retro.md +19 -4
  224. package/skills/quality-gates/SKILL.md +6 -4
  225. package/skills/remote-offload/SKILL.md +4 -2
  226. package/skills/session-end/learning-patterns.md +23 -13
  227. package/skills/session-end/metrics-collection.md +6 -7
  228. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  229. package/skills/session-end/plan-verification.md +25 -9
  230. package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
  231. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  232. package/skills/session-end/references/session-summary-template.md +1 -1
  233. package/skills/session-end/session-metrics-write.md +65 -68
  234. package/skills/session-plan/SKILL.md +0 -3
  235. package/skills/session-start/SKILL.md +8 -2
  236. package/skills/session-start/references/operations-contract.md +6 -0
  237. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  238. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  239. package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
  240. package/skills/test-audit/SKILL.md +152 -0
  241. package/skills/test-audit/references/campaign.md +111 -0
  242. package/skills/test-audit/references/mutate.sh +50 -0
  243. package/skills/vault-mirror/SKILL.md +21 -11
  244. package/skills/wave-executor/SKILL.md +2 -2
  245. package/skills/wave-executor/circuit-breaker.md +1 -1
  246. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  247. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
  248. package/skills/wave-executor/references/wave-loop-review.md +4 -6
  249. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  250. package/templates/personas/README.md +5 -5
  251. package/templates/personas/accounting-compliance.v1.md +1 -1
  252. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  253. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  254. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  255. package/templates/personas/buyer-p3-build.v1.md +1 -1
  256. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  257. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  258. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  259. package/templates/personas/klima-ai-expert.v1.md +1 -1
  260. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -4,7 +4,7 @@ description: >
4
4
  Use when detecting drift between CLAUDE.md (or AGENTS.md, the Codex CLI alias) / _meta narrative and
5
5
  live repository state. Ten checks: absolute-path resolution, 01-projects/ count claims, issue-reference
6
6
  freshness, session-file existence, command-count sync, session-config-parity (mandatory template keys =
7
- error, opt-in gaps = warning), vault-dir-parity (CLAUDE.md vs AGENTS.md), generated-rule-staleness
7
+ error, opt-in gaps = note, unknown local keys = warning), vault-dir-parity (CLAUDE.md vs AGENTS.md), generated-rule-staleness
8
8
  (WARN-only), rule-scoping (paths:/globs: frontmatter defects, dangling rule citations, zero-match
9
9
  globs), and docs-parity (docs/components.md count-claims vs on-disk counts, template-vs-reference
10
10
  config-key parity, stale .claude/metrics/ paths). Full per-check spec in the body table. Invoked as an
@@ -37,18 +37,18 @@ PHASE 1 IMPLEMENTED (2026-04-19). Session-end opt-in quality gate. Upstream of `
37
37
  | 2 | `project-count-sync` | Hardcoded "N registered" / "N projects" claims next to `01-projects/` | compare to `ls -d 01-projects/*/` |
38
38
  | 3 | `issue-reference-freshness` | `#NN` in forward-looking sections (What's Next, Backlog, Open Issues, Offene Themen, Todo, Next Steps) | `glab issue view NN --repo <origin>` |
39
39
  | 4 | `session-file-existence` | `50-sessions/YYYY-MM-DD-*.md` references anywhere in scope | `existsSync(vault/50-sessions/<file>)` |
40
- | 5 | `command-count` | "N commands" / "N /commands" claims in prose | compare to `ls commands/*.md \| wc -l`; skipped if no `commands/` dir |
41
- | 6 | `session-config-parity` | Top-level keys under `## Session Config` in `CLAUDE.md` / `AGENTS.md` | diff against `docs/session-config-template.md`; a missing MANDATORY (minimal-baseline) key is an error, a missing OPT-IN-baseline-only key is a warning |
40
+ | 5 | `command-count` | "N commands" / "N /commands" claims in prose | compare to the name-deduplicated union of `commands/*.md` and skills with explicit `user-invocable: true`; skipped only when both directories are absent |
41
+ | 6 | `session-config-parity` | Top-level keys under `## Session Config` in `CLAUDE.md` / `AGENTS.md` | diff against `docs/session-config-template.md`; a missing MANDATORY (minimal-baseline) key is an error, a missing OPT-IN-baseline-only key is a note, a local key absent from the template's Session Config blocks is a warning |
42
42
  | 7 | `vault-dir-parity` | `vault-integration.vault-dir` in BOTH `CLAUDE.md` AND `AGENTS.md` | reuse `_parseVaultIntegration`; flag when the two files disagree. Short-circuits to PASS when the two are the SAME file by construction (symlink / same inode / byte-identical generated copy) |
43
43
  | 8 | `generated-rule-staleness` *(WARN only)* | `.claude/rules/*.md` with `auto-generated: true` frontmatter | extract `learning-key`; WARN when the key is absent from `.orchestrator/metrics/learnings.jsonl` or its learning's `expires_at` is in the past; skipped silently when no auto-generated rules exist |
44
- | 9 | `rule-scoping` | `.claude/rules/*.md` frontmatter + `## See Also` footers + `.claude/rules/<name>.md` citations in `CLAUDE.md`/`AGENTS.md` | six probes: `paths:` frontmatter (error), cited-but-missing rule citations (error), zero-match `globs:` patterns (warn), foreign PascalCase glob tokens (warn), unreadable rule files (warn — surfaced instead of silently skipped), declared fleet-intent globs (**note**, never warn); skipped silently when `.claude/rules/` is absent |
44
+ | 9 | `rule-scoping` | `.claude/rules/*.md` frontmatter + `## See Also` footers + `.claude/rules/<name>.md` citations in `CLAUDE.md`/`AGENTS.md` | six probes: `paths:` frontmatter incl. frontmatter not at line 1 (error, #1449), cited-but-missing rule citations (error), zero-match `globs:` patterns (warn), foreign PascalCase glob tokens (warn), unreadable rule files (warn — surfaced instead of silently skipped), declared fleet-intent globs (**note**, never warn); skipped silently when `.claude/rules/` is absent |
45
45
  | 10 | `docs-parity` | `docs/components.md` count-claims, Session Config key parity (`docs/session-config-template.md` vs `docs/session-config-reference.md`), `.claude/metrics/` stale-path references in `docs/*.md` / `docs/examples/*.md` | three sub-checks (a/b/c) — see below; skipped silently when `docs/components.md` is absent |
46
46
 
47
47
  Check 3 deliberately scopes to forward-looking sections. Mentions inside "Recently Closed", "Decisions", "Archive", etc. describe history and must not be flagged.
48
48
 
49
- Check 5 counts `*.md` files directly inside `commands/` (non-recursive, non-hidden). The `commands/` directory is resolved relative to `VAULT_DIR` by default; use `--commands-dir <path>` to override.
49
+ Check 5 uses `countSlashCommands()` to count the union of non-hidden `*.md` files directly inside `commands/` and skills under `skills/` whose frontmatter explicitly declares `user-invocable: true`, deduplicated by name. The `commands/` directory is resolved relative to `VAULT_DIR` by default; `--commands-dir <path>` overrides that side of the union, while skills remain relative to `VAULT_DIR`. Either directory alone is sufficient; an existing directory with no commands contributes zero rather than causing a skip.
50
50
 
51
- Check 6 (issue #30; severity split per #785) extracts the YAML block under `## Session Config` from both the canonical template (`docs/session-config-template.md` by default, override with `--config-template`) and the resolved local instruction file — reading the template TWICE, once per occurrence: `{ occurrence: 'first' }` reaches the "Full minimal baseline" (the 7 schema-mandatory keys), `{ occurrence: 'last' }` reaches the "Full opt-in baseline" (the full key catalog, a strict superset of the minimal block). A template key missing from the local file is a `session-config-parity` **error** when it belongs to the minimal-baseline set, and a **warning** when it is opt-in-baseline-only — a consumer repo that legitimately does not adopt an opt-in feature (e.g. no `handover-gate`) must not go red; only the true 7-key mandatory contract does. When the template carries a single `## Session Config` heading (no separate opt-in block), `'first'` and `'last'` resolve to the same block and every key is treated as mandatory, preserving prior behaviour for single-block templates. Both fenced YAML (```` ```yaml ... ``` ````) and raw YAML body (up to next `## ` heading) are accepted. The check skips gracefully when the template file is absent, when no instruction file is detected, or when explicitly disabled via `--skip-session-config-parity`.
51
+ Check 6 (issue #30; severity split per #785 and #1356) extracts the YAML block under `## Session Config` from both the canonical template (`docs/session-config-template.md` by default, override with `--config-template`) and the resolved local instruction file — reading the template TWICE, once per occurrence: `{ occurrence: 'first' }` reaches the "Full minimal baseline" (the 7 schema-mandatory keys), `{ occurrence: 'last' }` reaches the "Full opt-in baseline" (the full key catalog, a strict superset of the minimal block). A template key missing from the local file is a `session-config-parity` **error** when it belongs to the minimal-baseline set, and a **note** with `probe: "opt-in-gap"` when it is opt-in-baseline-only. A local key absent from those template blocks produces a **warning**: it may be a typo or a template gap, and this check makes no claim about documentation elsewhere or runtime use. When the template carries a single `## Session Config` heading (no separate opt-in block), `'first'` and `'last'` resolve to the same block, every template key is treated as mandatory, and unknown-local-key warnings are skipped. Both fenced YAML (```` ```yaml ... ``` ````) and raw YAML body (up to next `## ` heading) are accepted. The check skips gracefully when the template file is absent, when no instruction file is detected, or when explicitly disabled via `--skip-session-config-parity`.
52
52
 
53
53
  The parity set is **template-driven**: every column-0 YAML key under `## Session Config` in the template (across both occurrences) is checked. As of the gsd Pattern Adoption Quick-Wins bundle (PRD 2026-05-22, issues #517–#521), the template-side keys include four opt-in top-level blocks:
54
54
 
@@ -57,7 +57,7 @@ The parity set is **template-driven**: every column-0 YAML key under `## Session
57
57
  - `templates-first` (Pattern 3 / #519) — gh/glab template-read enforcement hook
58
58
  - `verification-auto-fix` (Pattern 4 / #521) — opt-in auto-fix retry loop after Quality-Gate fail
59
59
 
60
- A local CLAUDE.md / AGENTS.md that omits any of these now surfaces a `session-config-parity` **warning** (not an error) — they are opt-in-baseline-only keys, so omitting them never trips `mode: hard`, and never blocks `autonomous-gated` skill-evolution's `runConfigValidationGate()` (which runs the checker with `--mode hard` and reads only `errors[]`).
60
+ A local CLAUDE.md / AGENTS.md that omits any of these surfaces a `session-config-parity` **note** — they are opt-in-baseline-only keys, so omitting them never trips `mode: hard`, and never blocks `autonomous-gated` skill-evolution's `runConfigValidationGate()` (which runs the checker with `--mode hard` and reads only `errors[]`).
61
61
 
62
62
  Check 7 (issue #600) is the **only** check that intentionally reads BOTH instruction files rather than the single alias-resolved one. The alias rule (CLAUDE.md wins ties, AGENTS.md is the Codex alias) means `resolveInstructionFile()` picks exactly one — so a repo carrying both files can silently let `AGENTS.md` drift out of sync with `CLAUDE.md`. A sibling project ran for weeks with a correct `vault-integration.vault-dir` in `CLAUDE.md` and a dead path in `AGENTS.md`. Check 7 reads `vault-integration.vault-dir` from each file (reusing the `_parseVaultIntegration` parser from `scripts/lib/config/vault-integration.mjs` — no hand-rolled YAML) and flags a `vault-dir-parity` error when the two values diverge (the error is attributed to `AGENTS.md`, the secondary alias, and names both values). The check skips gracefully when only one instruction file is present (nothing to compare), when neither file declares a `vault-integration:` block, or when explicitly disabled via `--skip-vault-dir-parity`. Two files that both omit `vault-dir` (both unset) agree and pass.
63
63
 
@@ -65,11 +65,11 @@ Check 7 (issue #600) is the **only** check that intentionally reads BOTH instruc
65
65
 
66
66
  The same predicate guards Check 9's probe 2a, which iterates `['CLAUDE.md', 'AGENTS.md']`: with an aliased `AGENTS.md` it scans `CLAUDE.md` only, because otherwise every dangling `.claude/rules/*.md` citation would be reported TWICE — one defect, two errors, and in `--mode strict` a doubled blocking surface against a file the operator is not allowed to edit.
67
67
 
68
- Check 9 (`rule-scoping`) validates `.claude/rules/*.md` frontmatter against the `scripts/lib/rule-loader.mjs` contract, catching the class of defect where a rule silently drifts out of the activation pipeline the loader actually implements. Six probes: **(1) paths-presence** — since #795 `paths:` is a full same-shape alias for `globs:` in `rule-loader.mjs` (`globs:` wins silently when both are present), so a well-formed `paths:`-only rule loads correctly SCOPED and is NOT flagged (#840). The probe fires only when the frontmatter textually carries a top-level `paths:` key AND `parseGlobsFrontmatter` — the loader's own parser, imported rather than re-derived — returns `globs === null` (e.g. a malformed frontmatter line makes it throw); that parse mismatch means the rule may silently load ALWAYS-ON, and is flagged as an error. **(2) cited-but-missing** — `(a)` `.claude/rules/<name>.md` citations inside `CLAUDE.md`/`AGENTS.md` that don't resolve to a file on disk, and `(b)` bare `<name>.md` tokens in a rule's own `## See Also` footer that don't exist as sibling rule files (tokens carrying a path separator, e.g. `../../skills/_shared/state-ownership.md`, are cross-directory references and explicitly out of scope); both flagged as errors. **(3) zero-match-globs** — a `globs:` pattern matching zero files in `git ls-files` (falls back to a manual directory walk when git is unavailable); flagged as a WARNING, not an error, because library/exemplar repos legitimately carry dead stack rules. **(4) foreign-glob** — a glob pattern containing a PascalCase product-like token (regex `[A-Z][a-z]+[A-Z]`, e.g. `WalkAITalkieTests`) — a likely copy-paste leftover from another project's rule scope; flagged as a WARNING. **(5) unreadable-file** — a rule file that could not be read (permissions, a race with a concurrent delete); flagged as a WARNING rather than silently skipped, so a completeness audit does not drop files, and so an unreadable file cannot brick the gate under `mode: hard`. **(6) fleet-intent-glob** — a `globs:` pattern DECLARED as fleet intent goes to `notes[]` (see below), never to `warnings[]`, and the declaration also settles probe 4 for that same pattern: a pattern the rule author declared is by construction not a copy-paste leftover. Glob matching reuses the same picomatch-with-inline-fallback resolution `scripts/lib/rule-loader.mjs` uses (`parseGlobsFrontmatter` is imported directly; the picomatch resolution itself is duplicated locally since `rule-loader.mjs` does not export a public matcher function). The check is skipped silently (no `checks_run` entry, no `checks_skipped` entry) when `.claude/rules/` is absent, or explicitly via `--skip-rule-scoping`.
68
+ Check 9 (`rule-scoping`) validates `.claude/rules/*.md` frontmatter against the `scripts/lib/rule-loader.mjs` contract, catching the class of defect where a rule silently drifts out of the activation pipeline the loader actually implements. Six probes: **(1) paths-presence** — a frontmatter block that does not open on line 1 (text or a provenance comment before `---`) is an ERROR since #1449: Claude Code ignores it and loads the rule always-on; the probe reports `parseGlobsFrontmatter(...).defect === 'frontmatter-not-at-top'` with the remedy (move the block to line 1, provenance comment after the closing `---`). Otherwise, since #795 `paths:` is a full same-shape alias for `globs:` in `rule-loader.mjs` (`globs:` wins silently when both are present), so a well-formed `paths:`-only rule loads correctly SCOPED and is NOT flagged (#840). The probe fires only when the frontmatter textually carries a top-level `paths:` key AND `parseGlobsFrontmatter` — the loader's own parser, imported rather than re-derived — returns `globs === null` (e.g. a malformed frontmatter line makes it throw); that parse mismatch means the rule may silently load ALWAYS-ON, and is flagged as an error. **(2) cited-but-missing** — `(a)` `.claude/rules/<name>.md` citations inside `CLAUDE.md`/`AGENTS.md` that don't resolve to a file on disk, and `(b)` bare `<name>.md` tokens in a rule's own `## See Also` footer that don't exist as sibling rule files (tokens carrying a path separator, e.g. `../../skills/_shared/state-ownership.md`, are cross-directory references and explicitly out of scope); both flagged as errors. **(3) zero-match-globs** — a `globs:` pattern matching zero files in `git ls-files` (falls back to a manual directory walk when git is unavailable); flagged as a WARNING, not an error, because library/exemplar repos legitimately carry dead stack rules. **(4) foreign-glob** — a glob pattern containing a PascalCase product-like token (regex `[A-Z][a-z]+[A-Z]`, e.g. `WalkAITalkieTests`) — a likely copy-paste leftover from another project's rule scope; flagged as a WARNING. **(5) unreadable-file** — a rule file that could not be read (permissions, a race with a concurrent delete); flagged as a WARNING rather than silently skipped, so a completeness audit does not drop files, and so an unreadable file cannot brick the gate under `mode: hard`. **(6) fleet-intent-glob** — a `globs:` pattern DECLARED as fleet intent goes to `notes[]` (see below), never to `warnings[]`, and the declaration also settles probe 4 for that same pattern: a pattern the rule author declared is by construction not a copy-paste leftover. Glob matching reuses the same picomatch-with-inline-fallback resolution `scripts/lib/rule-loader.mjs` uses (`parseGlobsFrontmatter` is imported directly; the picomatch resolution itself is duplicated locally since `rule-loader.mjs` does not export a public matcher function). The check is skipped silently (no `checks_run` entry, no `checks_skipped` entry) when `.claude/rules/` is absent, or explicitly via `--skip-rule-scoping`.
69
69
 
70
70
  **Fleet-intent declaration (Check 9, probe 6).** A rule may declare that a `globs:` pattern is aimed at CONSUMER repos rather than this one, via its own frontmatter key `fleet-intent-globs:`. Accepted forms are block style (`fleet-intent-globs:` plus indented ` - "<pattern>"` lines), flow style (`fleet-intent-globs: ["a", "b"]`) and a single inline value; surrounding quotes are stripped, blank and `#` comment lines are ignored. The key is inert for rule loading — `rule-loader.mjs`'s `parseGlobsFrontmatter` skips unknown top-level keys and their continuation lines. A declared pattern is reported in `notes[]` with its file, the pattern, whether it matches `≥1` or `0` tracked files here, and the declaration `source` (`frontmatter`). The grain is per pattern AND per rule file: declaring one glob amnesties neither a sibling glob in the same rule nor the same pattern in another rule. There is deliberately NO checker-side exemption list: a rule that does not declare the pattern itself is not exempt, so the declaration always sits with the rule it describes.
71
71
 
72
- Check 10 (`docs-parity`, issue #780) validates the public docs surface against live repository state via three sub-checks, all reported under the single `docs-parity` check id: **(a) count-claims** — `docs/components.md`'s own heading counts ("## Skills (N user-facing)", "## Commands (N)", "## Agents (N typed sub-agents)", "## Hook event types (N)") are compared against the SAME actual on-disk derivation the surface-count family (Check 5) uses (`countSkills`/the commands-dir listing/`countAgents`/`readHookCounts`) — NOT Check 5's `claimRe` regexes, which are tuned for CLAUDE.md/README prose phrasing and verifiably do not match `components.md`'s own heading convention; a dedicated set of regexes tailored to the doc's actual authored structure is used instead. **(b) config-block-parity** — top-level Session Config keys documented in `docs/session-config-template.md` (opt-in baseline, via the Check 6 'last'-occurrence extractor) are compared against `docs/session-config-reference.md`; a key is considered documented in the reference when it appears in a `yaml` fence (any indent), a `##`/`###`/`####` heading, or the first cell of a markdown table row (`` | `key-name` | ... ``) — the reference's dominant documentation shape is one table row per field, so a naive fence-only extraction under-counts what it actually documents. Missing keys surface as `docs-parity` errors attributed to `docs/session-config-reference.md`. **(c) metrics-path-liveness** — any `.claude/metrics/` path reference (stale; canonical is `.orchestrator/metrics/`) found in root `docs/*.md` or `docs/examples/*.md` surfaces as an error at its exact file/line. The whole check is skipped silently (no `checks_run` entry, no `checks_skipped` entry) when `docs/components.md` is absent, or explicitly via `--skip-docs-parity`.
72
+ Check 10 (`docs-parity`, issue #780) validates the public docs surface against live repository state via three sub-checks, all reported under the single `docs-parity` check id: **(a) count-claims** — `docs/components.md`'s own heading counts ("## Skills (N user-facing)", "## Commands (N)", "## Agents (N typed sub-agents)", "## Hook event types (N)") are compared against the SAME actual on-disk derivation the surface-count family (Check 5) uses (`countSkills`/`countSlashCommands`/`countAgents`/`readHookCounts`) — NOT Check 5's `claimRe` regexes, which are tuned for CLAUDE.md/README prose phrasing and verifiably do not match `components.md`'s own heading convention; a dedicated set of regexes tailored to the doc's actual authored structure is used instead. **(b) config-block-parity** — top-level Session Config keys documented in `docs/session-config-template.md` (opt-in baseline, via the Check 6 'last'-occurrence extractor) are compared against `docs/session-config-reference.md`; a key is considered documented in the reference when it appears in a `yaml` fence (any indent), a `##`/`###`/`####` heading, or the first cell of a markdown table row (`` | `key-name` | ... ``) — the reference's dominant documentation shape is one table row per field, so a naive fence-only extraction under-counts what it actually documents. Missing keys surface as `docs-parity` errors attributed to `docs/session-config-reference.md`. **(c) metrics-path-liveness** — any `.claude/metrics/` path reference (stale; canonical is `.orchestrator/metrics/`) found in root `docs/*.md` or `docs/examples/*.md` surfaces as an error at its exact file/line. The whole check is skipped silently (no `checks_run` entry, no `checks_skipped` entry) when `docs/components.md` is absent, or explicitly via `--skip-docs-parity`.
73
73
 
74
74
  ## Files
75
75
 
@@ -133,7 +133,7 @@ Environment:
133
133
  }
134
134
  ```
135
135
 
136
- `notes[]` is the THIRD output category, alongside `errors[]` and `warnings[]`: a finding that is reported but asks for no action. It never affects `status` or the exit code, and it is always present (empty array when nothing was noted, including on the short-circuit "no scope files matched" path). Its only producer today is Check 9's fleet-intent-glob probe.
136
+ `notes[]` is the THIRD output category, alongside `errors[]` and `warnings[]`: a finding that is reported but asks for no action. It never affects `status` or the exit code, and it is always present (empty array when nothing was noted, including on the short-circuit "no scope files matched" path). Producers are Check 6's `opt-in-gap` probe and Check 9's `fleet-intent-glob` probe. Opt-in-gap notes carry `check: "session-config-parity"`, `probe: "opt-in-gap"`, the instruction file and line, a message, and the omitted key in `extracted`; they have no `source` field.
137
137
 
138
138
  The `resolved_path` / `resolved_kind` pair surfaces the alias resolution outcome (issue #33 AC2) so users on either platform can audit which instruction file the checker scanned. `kind: 'claude'` for `CLAUDE.md`, `kind: 'agents'` for `AGENTS.md`, `null` when neither was found.
139
139
 
@@ -25,7 +25,9 @@
25
25
  * Reuses `_parseVaultIntegration` from scripts/lib/config/ for Check 7;
26
26
  * reuses `parseGlobsFrontmatter` from scripts/lib/rule-loader.mjs for Check 9's
27
27
  * glob extraction (mirrors the same picomatch-with-inline-fallback resolution
28
- * rule-loader.mjs uses); otherwise pure Node stdlib.
28
+ * rule-loader.mjs uses) and its line-1-only frontmatter rule (#1449: a block
29
+ * with anything before its opening `---` is not frontmatter, exactly as Claude
30
+ * Code reads it); otherwise pure Node stdlib.
29
31
  */
30
32
 
31
33
  import { readFileSync, readdirSync, existsSync, statSync, lstatSync, realpathSync } from 'node:fs';
@@ -36,7 +38,7 @@ import { resolveInstructionFile } from '../../scripts/lib/common.mjs';
36
38
  import { _parseVaultIntegration } from '../../scripts/lib/config/vault-integration.mjs';
37
39
  import { _parseDriftCheck } from '../../scripts/lib/config/drift-check.mjs';
38
40
  import { isSessionConfigHeading } from '../../scripts/lib/config/section-extractor.mjs';
39
- import { parseGlobsFrontmatter } from '../../scripts/lib/rule-loader.mjs';
41
+ import { FRONTMATTER_NOT_AT_TOP, parseGlobsFrontmatter } from '../../scripts/lib/rule-loader.mjs';
40
42
  import { resolveRepoSpec } from '../../scripts/lib/vcs-repo-spec.mjs';
41
43
  import { userInvocableSkills } from '../../scripts/lib/user-invocable-skills.mjs';
42
44
 
@@ -573,7 +575,11 @@ function lookupIssueState(iid, repo, cache, vaultDir) {
573
575
  // rule-loader.mjs's parseGlobsFrontmatter (the `paths:` alias, issue
574
576
  // #795) fails to recognise — a genuine parse mismatch. A well-formed
575
577
  // `paths:`-only rule is NOT flagged: since #795, `paths:` is a full
576
- // alias for `globs:` and loads correctly SCOPED (#840).
578
+ // alias for `globs:` and loads correctly SCOPED (#840). Also an error
579
+ // (#1449): a frontmatter block that does not start on line 1 (a
580
+ // provenance comment, a blank line or any text before the opening
581
+ // `---`) — parseGlobsFrontmatter reports it as `frontmatter-not-at-top`,
582
+ // Claude Code ignores such a block, and the rule loads ALWAYS-ON.
577
583
  // 2. cited-but-missing → errors[]: (a) `.claude/rules/<name>.md` citations in
578
584
  // CLAUDE.md/AGENTS.md that don't exist on disk; (b) bare `<name>.md`
579
585
  // tokens in a rule's own `## See Also` footer that don't exist on disk
@@ -674,21 +680,12 @@ function listTrackedFiles(vaultDir) {
674
680
  }
675
681
  }
676
682
 
677
- const RULE_HEADER_LINE_RE = /^[ \t]*(?:<!--.*-->)?[ \t]*$/;
678
-
679
- function stripLeadingRuleHeaderLines(content) {
680
- const lines = content.split(/\r?\n/);
681
- let idx = 0;
682
- while (idx < lines.length && RULE_HEADER_LINE_RE.test(lines[idx])) idx++;
683
- return lines.slice(idx).join('\n');
684
- }
685
-
686
683
  /** Extracts the raw frontmatter block body (text between the `---` delimiters),
687
- * or null when the file has no frontmatter block. Leading blank/comment
688
- * provenance lines are tolerated so Check 9 sees the same frontmatter shape
689
- * that rule-loader.mjs accepts. */
684
+ * or null when the file has no frontmatter block. Only a block whose opening
685
+ * `---` is line 1 counts (#1449) — the rule rule-loader.mjs and Claude Code
686
+ * apply; a displaced block is body text, reported by probe 1 instead. */
690
687
  function extractFrontmatterBlockBody(content) {
691
- const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(stripLeadingRuleHeaderLines(content));
688
+ const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content);
692
689
  return m ? m[1] : null;
693
690
  }
694
691
 
@@ -1173,7 +1170,7 @@ function main() {
1173
1170
  // Minimal inline frontmatter extractor for auto-generated + learning-key.
1174
1171
  // Reads the opening --- ... --- block from a markdown file.
1175
1172
  function extractFrontmatterFields(mdContent) {
1176
- const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(stripLeadingRuleHeaderLines(mdContent));
1173
+ const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(mdContent);
1177
1174
  if (!m) return { autoGenerated: false, learningKey: null, expiresAt: null, evidenceDigest: null };
1178
1175
  const block = m[1];
1179
1176
  const autoGenM = /^auto-generated:\s*(.+)$/m.exec(block);
@@ -1370,10 +1367,21 @@ function main() {
1370
1367
  // parseGlobsFrontmatter failed to recognise it (globs === null) —
1371
1368
  // a genuine parse mismatch between this probe's textual detection
1372
1369
  // and the loader's actual behaviour, not routine `paths:` usage.
1370
+ // #1449: the same parser also reports a frontmatter block that does
1371
+ // not start on line 1. Claude Code ignores that block, so whatever
1372
+ // key it carries (globs:, paths:, tier:, …) has no effect and the
1373
+ // rule loads ALWAYS-ON — an error of its own, reported instead of
1374
+ // the paths: mismatch (fmBody is null for such a file anyway).
1373
1375
  let parsed;
1374
1376
  try { parsed = parseGlobsFrontmatter(content); } catch { parsed = { globs: null, meta: {} }; }
1375
1377
  const fmBody = extractFrontmatterBlockBody(content);
1376
- if (fmBody && /^paths:/m.test(fmBody) && parsed.globs === null) {
1378
+ if (parsed.defect === FRONTMATTER_NOT_AT_TOP) {
1379
+ errors.push({
1380
+ check: 'rule-scoping', file: relPath, line: 1,
1381
+ message: `${relPath}: frontmatter is not at line 1 (text before the opening ---) — Claude Code ignores it, the rule loads ALWAYS-ON; move the frontmatter to line 1 and put the provenance comment after the closing ---`,
1382
+ extracted: FRONTMATTER_NOT_AT_TOP,
1383
+ });
1384
+ } else if (fmBody && /^paths:/m.test(fmBody) && parsed.globs === null) {
1377
1385
  errors.push({
1378
1386
  check: 'rule-scoping', file: relPath, line: 1,
1379
1387
  message: `Rule frontmatter declares 'paths:' but rule-loader.mjs's parseGlobsFrontmatter did not recognise it — the rule may silently load ALWAYS-ON. Verify the paths:/globs: frontmatter syntax.`,
@@ -24,7 +24,8 @@
24
24
  * 2. `.claude/rules/*.md` file count (non-recursive) vs. "N rule files" claims.
25
25
  * 3. `skills/` user-facing directory count (excludes `_shared/`) vs.
26
26
  * "N user-facing skills" / "N skills" claims.
27
- * 4. `commands/*.md` file count vs. "N slash-commands" / "N commands" claims.
27
+ * 4. Slash-command count (`commands/*.md` ∪ skills with `user-invocable: true`)
28
+ * vs. "N slash-commands" / "N commands" claims.
28
29
  *
29
30
  * Scope exclusions: CHANGELOG.md, docs/adr/, docs/prd/, docs/retro/ are never
30
31
  * scanned — same principle as the docs-staleness probe (historical/immutable
@@ -45,6 +46,7 @@
45
46
 
46
47
  import { existsSync, readdirSync, readFileSync, mkdirSync, appendFileSync } from 'node:fs';
47
48
  import { join } from 'node:path';
49
+ import { slashCommandNames } from '../../../scripts/lib/user-invocable-skills.mjs';
48
50
 
49
51
  // ---------------------------------------------------------------------------
50
52
  // Doc source sets (deliberately small and hand-curated)
@@ -101,12 +103,16 @@ function countUserFacingSkills(root) {
101
103
  }
102
104
  }
103
105
 
106
+ // Slash commands = `commands/*.md` ∪ skills with an explicit
107
+ // `user-invocable: true` (the #1370 commands→skills fold) — the shared census
108
+ // in user-invocable-skills.mjs, not a second copy. Counting `commands/` alone
109
+ // reported 2 against the real 27 on every run (#1506 pt 7). `null` (entry
110
+ // skipped) only when the repo has neither source.
104
111
  function countCommands(root) {
105
- const dir = join(root, 'commands');
106
- if (!existsSync(dir)) return null;
107
112
  try {
108
- const entries = readdirSync(dir, { withFileTypes: true });
109
- return entries.filter((e) => e.isFile() && e.name.endsWith('.md')).length;
113
+ const names = slashCommandNames(root);
114
+ if (names.length === 0 && !existsSync(join(root, 'commands'))) return null;
115
+ return names.length;
110
116
  } catch {
111
117
  return null;
112
118
  }
@@ -180,7 +186,7 @@ const REGISTRY = [
180
186
  },
181
187
  {
182
188
  id: 'commands-count',
183
- label: 'commands/*.md file count',
189
+ label: 'slash-command count (commands/*.md + user-invocable skills)',
184
190
  sources: SOURCE_SET_INVENTORY,
185
191
  severity: 'medium',
186
192
  getActual: countCommands,
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { existsSync, readFileSync, readdirSync, mkdirSync, appendFileSync } from 'node:fs';
26
26
  import { join } from 'node:path';
27
+ import { expandTilde } from '../../../scripts/lib/common.mjs';
27
28
 
28
29
  // ---------------------------------------------------------------------------
29
30
  // Constants
@@ -73,9 +74,12 @@ function parseFrontmatter(text) {
73
74
  // Helpers
74
75
  // ---------------------------------------------------------------------------
75
76
 
77
+ // #1506 pt 6: the committed value is `~/Projects/vault`; unexpanded it resolved
78
+ // against the cwd (`<cwd>/~/Projects/vault`), where a stray `~` directory
79
+ // produced a false all-clear (0 stale narratives instead of 33).
76
80
  function resolveVaultDir(projectRoot, config) {
77
- if (config?.['vault-integration']?.['vault-dir']) return config['vault-integration']['vault-dir'];
78
- if (process.env.VAULT_DIR) return process.env.VAULT_DIR;
81
+ if (config?.['vault-integration']?.['vault-dir']) return expandTilde(config['vault-integration']['vault-dir']);
82
+ if (process.env.VAULT_DIR) return expandTilde(process.env.VAULT_DIR);
79
83
  return null;
80
84
  }
81
85
 
@@ -38,6 +38,7 @@
38
38
 
39
39
  import { existsSync, readFileSync, readdirSync, mkdirSync, appendFileSync } from 'node:fs';
40
40
  import { join } from 'node:path';
41
+ import { expandTilde } from '../../../scripts/lib/common.mjs';
41
42
 
42
43
  // ---------------------------------------------------------------------------
43
44
  // Minimal YAML frontmatter parser — top-level scalar fields only (<30 lines).
@@ -112,13 +113,18 @@ export async function runProbe(projectRoot, config) {
112
113
  try {
113
114
  // --- Config validation (early exit paths) ---
114
115
 
115
- const vaultDir = config?.['vault-integration']?.['vault-dir'];
116
- if (!vaultDir) {
116
+ const configuredVaultDir = config?.['vault-integration']?.['vault-dir'];
117
+ if (!configuredVaultDir) {
117
118
  return makeSkip('vault-dir not configured');
118
119
  }
120
+ // #1506 pt 6: the committed value is `~/Projects/vault`; unexpanded it
121
+ // resolved against the cwd (`<cwd>/~/Projects/vault`), where a stray `~`
122
+ // directory produced a false all-clear (0 projects scanned instead of 45).
123
+ const vaultDir = expandTilde(configuredVaultDir);
119
124
 
120
125
  if (!existsSync(vaultDir)) {
121
- return makeSkip(`vault-dir does not exist: ${vaultDir}`);
126
+ // The configured spelling, not the expanded one: no home dir in the output.
127
+ return makeSkip(`vault-dir does not exist: ${configuredVaultDir}`);
122
128
  }
123
129
 
124
130
  const projectsDir = join(vaultDir, '01-projects');
@@ -177,9 +177,15 @@ No Docs task generated for this audience.
177
177
  documents what the docs-writer MUST do when executing a dispatched Docs task.
178
178
 
179
179
  Before writing any documentation, the docs-writer MUST read and ingest the actual
180
- source material provided in the task prompt. The canonical four source types are:
181
-
182
- 1. **diff** — `git diff $SESSION_START_REF..HEAD` — the verbatim diff of all changes
180
+ source material provided in the task prompt. The canonical four source types are
181
+ below. `$SESSION_RANGE` is ONE complete diff range: inside a wave run it is
182
+ `"$SESSION_START_REF..HEAD"` (the SHA wave-executor pins at Pre-Wave 1a); at
183
+ session end it is what the accessor in `skills/session-end/plan-verification.md`
184
+ § SESSION_START_REF accessor exports — the pinned SHA, else the resolved
185
+ `<base>...HEAD`, never a hard-coded `origin/main`. Diffs take the range; the log
186
+ keeps the two-dot `$SESSION_START_REF..HEAD`, which is the correct form for `git log`.
187
+
188
+ 1. **diff** — `git diff "$SESSION_RANGE"` — the verbatim diff of all changes
183
189
  made during this session. This is the primary authoritative source for what changed.
184
190
  2. **git-log** — `git log $SESSION_START_REF..HEAD --format="%H %s%n%b"` — commit
185
191
  messages and associated bodies. Secondary context for all audiences; use to
@@ -284,9 +290,10 @@ The docs-writer MUST emit a brief structured output summary:
284
290
  1. Collect every task classified as `Docs` in the plan.
285
291
  2. For each task, resolve the expected `file-pattern` target and check for a diff:
286
292
  ```bash
287
- git diff --name-only $SESSION_START_REF..HEAD
293
+ git diff --name-only "$SESSION_RANGE"
288
294
  ```
289
- Match the output against the task's file-pattern using glob matching.
295
+ (`$SESSION_RANGE` from the session-end accessor above; an empty range is a skip
296
+ with a named reason, never "no docs changed".) Match the output against the task's file-pattern using glob matching.
290
297
  3. Classify gaps by severity:
291
298
  - **missing diff** — no matching file was changed; the task did not run or produced
292
299
  no output.
@@ -23,14 +23,16 @@ traceable source gets `<!-- REVIEW: source needed -->` — the agent NEVER inven
23
23
 
24
24
  | Source | Command / Path | Primary audience(s) | Notes |
25
25
  |--------|---------------|---------------------|-------|
26
- | **diff** | `git diff $SESSION_START_REF..HEAD` | Dev, Vault | Authoritative for what changed and when. Preferred primary source for Dev. |
26
+ | **diff** | `git diff "$SESSION_RANGE"` | Dev, Vault | Authoritative for what changed and when. Preferred primary source for Dev. |
27
27
  | **git-log** | `git log $SESSION_START_REF..HEAD --format="%H %s%n%b"` | All | Secondary context; use to reconstruct the "why" when diffs alone are ambiguous. |
28
28
  | **session-memory** | `~/.claude/projects/<project>/memory/session-*.md`, `.orchestrator/` outputs | Vault, User | Primary for Vault narratives (status updates, decisions made in conversation). Also primary for User tasks designed interactively. |
29
29
  | **affected-files** | Files in wave-scope `allowedPaths` / coordinator `affected-files` context block | User, Dev | Primary for understanding updated interfaces, config schemas, or module structures. |
30
30
 
31
+ `$SESSION_RANGE` is one complete diff range — `"$SESSION_START_REF..HEAD"` inside a wave run, the session-end accessor's export at close (`../session-end/plan-verification.md` § SESSION_START_REF accessor; never a hard-coded `origin/main`). The log keeps the two-dot `$SESSION_START_REF..HEAD`, the correct form for `git log`.
32
+
31
33
  **Detailed source descriptions:**
32
34
 
33
- 1. **diff** — Direct code changes from `git diff $SESSION_START_REF..HEAD`. Authoritative
35
+ 1. **diff** — Direct code changes from `git diff "$SESSION_RANGE"`. Authoritative
34
36
  for Dev narratives (what changed and why) and Vault decisions (what was decided, when).
35
37
  Preferred primary source for Dev audience tasks.
36
38
  2. **git-log** — Commit messages and associated PR/MR bodies from
@@ -90,7 +92,7 @@ Trigger: New `coordinator-snapshot.mjs` module added; CWD-drift guard introduced
90
92
  ## Allowed Sources
91
93
  Only these four sources are permitted. Do not use general knowledge.
92
94
 
93
- - diff: `git diff $SESSION_START_REF..HEAD` (provided below)
95
+ - diff: `git diff "$SESSION_RANGE"` (provided below)
94
96
  - git-log: `git log $SESSION_START_REF..HEAD --format="%H %s%n%b"` (provided below)
95
97
  - session-memory: wave summaries and agent outputs from
96
98
  ~/.claude/projects/<project>/memory/session-*.md and .orchestrator/ (provided below)
@@ -171,18 +171,27 @@ If user selects "Confidence ändern", "Ablauf verlängern", or "Delete specific
171
171
 
172
172
  Use the same archive-safe pipeline as Phase 3, Step 3.5 — **never** a hand-rolled `>` rewrite (#1017):
173
173
 
174
- 1. Read all lines from `learnings.jsonl`
174
+ 1. Snapshot the store into the sidecar exactly as Phase 3, Step 3.5(1) does (`--prune --snapshot`,
175
+ with the same `--file` / `--archive` the apply will use) and work on the sidecar, never on a
176
+ separate read of `learnings.jsonl` (#1486). A snapshot exit `1` (a malformed store line, or a
177
+ PATH that would replace a ledger) wrote nothing: surface it and stop
175
178
  2. Apply the selected operation to selected learnings:
176
- - **Boost:** +0.15 confidence (cap 1.0), reset expires_at to +`learning-expiry-days`
179
+ - **Boost:** +0.15 confidence (cap 1.0), reset expires_at to `deriveExpiresAt(now, type)` (per-type TTL, `scripts/lib/learnings/schema.mjs`)
177
180
  - **Reduce:** -0.2 confidence
178
181
  - **Delete:** omit the selected entries from the next generation — do NOT delete them by hand.
179
182
  `pruneLearnings()` detects every **record** that left the store — reconciled by `id`, or by a
180
183
  content fingerprint when a record carries no usable `id` — and archives it with
181
184
  `_archive_reason: "pruned"`, so a `learning-id` referenced by a rendered rule stays resolvable.
182
- - **Extend:** reset expires_at to current date + `learning-expiry-days`
185
+ - **Extend:** reset expires_at to `deriveExpiresAt(now, type)` — `learning-expiry-days` is parsed but read by no script (`skills/_shared/config-reading.md` § Learning Expiry Semantics)
183
186
  3. Steps 3–5 of the old prose (prune / consolidate / rewrite) are `pruneLearnings()` — run the
184
187
  **exact** Step 3.5(5) invocation, writing the post-operation entry set to the `--entries`
185
- sidecar. It prunes
188
+ sidecar below its line-1 `_store_generation` header. Exit `1` or `2`: surface the error and
189
+ stop (`1` includes a sidecar snapshotted from another store than `--file`). Exit `3` means the
190
+ store changed after the snapshot and nothing was written: snapshot into a FRESH path,
191
+ re-apply the selected operations to the records in that file, and apply it once more — a
192
+ second consecutive `3`: stop and report. Never move the fresh header onto the old sidecar's
193
+ records, whose missing peer change the prune would archive `pruned` (Step 3.5(5) carries the
194
+ full exit-code rule). It prunes
186
195
  (`expires_at` < now → `expired`; `confidence <= 0.0` → `pruned`), consolidates duplicates
187
196
  (same `type` + non-empty `subject`, highest confidence wins, loser archived `superseded` with
188
197
  `_superseded_by`; null-subject entries preserved individually per #284), and rewrites through
@@ -61,10 +61,9 @@ For each of the 9 built-in analyzer learning types, apply these heuristics.
61
61
 
62
62
  - Compare `total_agents` and `total_waves` across session types
63
63
  - Calculate average agents per wave for each session type. Read a wave's agent count defensively — older records use other keys: `agent_count`, else `agents` when it is a number, else the length of `agents` when it is an array (of descriptions), else `agents_dispatched`. A record with `total_waves: 0` contributes no per-wave ratio — skip it rather than divide by zero. Exclude coordinator-direct waves (`coordinator_direct: true`, which dispatched no agents) from the ratio. In particular, a record whose waves are all coordinator-direct `Housekeeping` waves (predicate `isCoordinatorDirectHousekeeping` in `scripts/lib/session-schema/filters.mjs`, the session-end writer shape since #1321) contributes no per-wave ratio, same as `total_waves: 0`. Counting it would log a false 0.0 agents-per-wave observation.
64
- - **Group by `(session_type, session_profile)` (#1247), never by `session_type` alone.** `ultradeep` is a PROFILE over `session_type: deep` (7 waves vs. deep's 4-5, STATE.md frontmatter key `session-profile` — see `scripts/lib/state-md.mjs`), not a distinct type, so a bare `session_type` grouping folds a 7-wave ultradeep session and a 5-wave deep session into the same row and silently averages their agents-per-wave and over-delivery numbers together.
64
+ - **Group by `(session_type, session_profile)` (#1247), never by `session_type` alone.** `ultradeep` is a PROFILE over `session_type: deep` (7 waves vs. deep's 4-5, STATE.md frontmatter key `session-profile` — see `scripts/lib/state-md.mjs`), not a distinct type, so a bare `session_type` grouping folds a 7-wave ultradeep session and a 5-wave deep session into the same row and silently averages their agents-per-wave numbers together.
65
65
  - Subject = `sizingSubject({ session_type, session_profile })` from `scripts/lib/learnings/sizing-subject.mjs` — e.g. `deep-session-sizing` (no profile, byte-identical to the pre-#1247 literal), `deep-ultradeep-session-sizing` (profile `ultradeep`), or `feature-session-sizing`. Always derive via this helper; do not hand-concatenate the subject string.
66
66
  - Insight = "Deep sessions average X agents across Y waves" or "Ultradeep sessions average X agents across Y waves" (name the profile in the insight whenever `session_profile` is present, so the two rows read as distinct sizing observations, not variants of one sentence)
67
- - **Over-delivery ratio aggregation (#730/H4, #794.7; keyed by profile since #1247):** compute the MEDIAN of `waves[].over_delivery_ratio` across the last ~5 `sessions.jsonl` records of the same `(session_type, session_profile)` pair, filtered to waves whose `role` is not `Discovery`/`Finalization` and which carry the field (skip records lacking the field — pre-#730; also skip Discovery/Finalization waves, whose planned set is empty by design). This exclusion clause is intentionally identical to `skills/session-plan/SKILL.md` Step 0.5 "Over-delivery sizing" — keep the two wordings in sync on edit. Fold the median into this candidate's `insight`/`evidence` fields — e.g. `evidence`: `"median_over_delivery_ratio: 1.4 (n=12 waves, session_type=deep, session_profile=ultradeep)"` — so `session-plan` Step 0.5 can read the ratio from the `effective-sizing` learning first, falling back to its own direct `sessions.jsonl` scan only when no such learning exists.
68
67
 
69
68
  #### 3. recurring-issue (type: `recurring-issue`)
70
69
 
@@ -209,7 +208,7 @@ If user selects "Skip all" or selects nothing, abort gracefully: "No learnings s
209
208
 
210
209
  For confirmed learnings, use atomic rewrite strategy:
211
210
 
212
- 1. Read ALL existing lines from `.orchestrator/metrics/learnings.jsonl` (if exists) into memory. If not found, check `<state-dir>/metrics/learnings.jsonl` as a legacy fallback. If legacy data is found, it will be migrated to the v2 path on write (step 5).
211
+ 1. Snapshot the store into the sidecar: `node scripts/sweep-expired-learnings.mjs --prune --snapshot .orchestrator/tmp/learnings-next.jsonl`. It writes every record of `.orchestrator/metrics/learnings.jsonl` to that file behind a line-1 `{"_store_generation": "sha256:…", "_store_path": "…"}` header naming the exact store state and the store they came from. Read the SIDECAR, not the store, as your starting set — step 5 explains why. On a non-default store, pass the SAME `--file` / `--archive` here as to the step-5 apply: the apply refuses (exit `1`) a sidecar snapshotted from another store. A store holding a malformed (unparseable) line does NOT stop the snapshot: it prints a stderr WARN naming the line numbers and adds a `malformed` count to its summary. The sidecar carries records only, and the step-5 apply keeps the store's malformed lines verbatim at the end of the rewrite, never archived — report the count to the operator; never hand-edit the store to remove them. The one sanctioned repair is `node scripts/sweep-expired-learnings.mjs --drop-malformed --line N [--line M …]`: a dry run by default, printing each line's preview, any readable record it still carries (`bom` / `embedded_record`) and the store generation; only after the operator confirmed those previews, `--drop-malformed --line N --apply --generation <TOKEN> --repo-root "$(pwd)"` removes the lines (backup `<stem>.pre-drop-malformed.jsonl.bak-<ISO>` beside the store). A line still holding a readable record needs `--accept-embedded`; a store that changed since the dry run exits `3`. If the snapshot holds zero records, check `<state-dir>/metrics/learnings.jsonl` as a legacy fallback and add its records below line 1; they are migrated to the v2 path on write (step 5).
213
212
  2. Apply confidence updates for confirmed existing learnings:
214
213
  - Increment confidence by +0.15
215
214
  - Cap at 1.0
@@ -236,9 +235,22 @@ For confirmed learnings, use atomic rewrite strategy:
236
235
  deleted 11 of 13 `learning-id` provenance targets referenced by rendered `.claude/rules/*.md`.
237
236
  Do not hand-roll a `jq | ... > learnings.jsonl` pass; it bypasses every #721 safety net.
238
237
 
239
- Write the full next-generation entry set (existing entries **with** the step-2/3 confidence
240
- updates, **plus** the step-4 new learnings) as JSONL to a temp sidecar **via the Write tool**
241
- (not a shell `>` redirect — the destructive-command guard blocks it), then invoke the
238
+ Turn the step-1 sidecar `.orchestrator/tmp/learnings-next.jsonl` into the full
239
+ next-generation entry set (existing entries **with** the step-2/3 confidence updates,
240
+ **plus** the step-4 new learnings) **via the Write or Edit tool** (not a shell `>` redirect —
241
+ the destructive-command guard blocks it), **keeping line 1, the `_store_generation` header,
242
+ unchanged and first**. The sidecar lives under `.orchestrator/tmp/`, never
243
+ `.orchestrator/metrics/`, because `tmp/` is outside
244
+ the ledger guard's `ledger-delete-protected` denylist — a metrics-dir sidecar made the
245
+ cleanup `rm` below a blocked command (#1453).
246
+
247
+ **Snapshot → token → apply refuses on drift (#1486).** Minutes pass between step 1 and the
248
+ call below, and a peer session may append to the store meanwhile. Such a record is on disk
249
+ but absent from your sidecar, so the prune would archive it `pruned` — a delete nobody
250
+ decided, which the store lock cannot prevent because your read happened before it, in
251
+ another process. So the prune re-reads the store under its lock and compares that
252
+ generation with the header: any difference writes nothing and exits `3`. A sidecar without
253
+ the header exits `1` — the header is mandatory, never optional. Then invoke the
242
254
  `--prune` subcommand of the sweep CLI. **This call is also `/evolve`'s ONLY
243
255
  `orchestrator.evolve.completed` success emit (#1206)** — export `N` (Step 3.5(4)'s
244
256
  new-learnings count), `M` (Step 3.5(2)'s reinforced-existing count) and `DURATION_MS`
@@ -247,14 +259,15 @@ For confirmed learnings, use atomic rewrite strategy:
247
259
  an argument error:
248
260
 
249
261
  ```bash
250
- NEXT=".orchestrator/metrics/.learnings-next.jsonl" # written by the step above
262
+ NEXT=".orchestrator/tmp/learnings-next.jsonl" # written by the step above
251
263
  node scripts/sweep-expired-learnings.mjs --prune --apply --json --entries "$NEXT" \
252
264
  --appended "${N:-0}" --boosted "${M:-0}" --duration-ms "${DURATION_MS:-0}" \
253
265
  --repo-root "$(pwd)" && rm -f "$NEXT"
254
266
  ```
255
267
 
256
268
  `--file` / `--archive` default to the canonical store + archive paths — pass them only when
257
- operating on a non-default pair. The command prints ONE JSON line; capture it as `$PRUNE` and
269
+ operating on a non-default pair, and then pass the same pair to the step-1 `--snapshot` too.
270
+ The command prints ONE JSON line; capture it as `$PRUNE` and
258
271
  report its `{scanned, kept, archived, byReason}` in the final summary — `$PRUNE.archived` is
259
272
  also the `pruned` counter the emit above just wrote, so there is nothing left to compute for
260
273
  the telemetry after this line. Preview first with `--prune --dry-run --json` (same counts,
@@ -269,12 +282,22 @@ For confirmed learnings, use atomic rewrite strategy:
269
282
  > independently of the write it reported on. Do not re-inline either, and do not hand-roll a
270
283
  > `jq | ... > learnings.jsonl` pass — that bypasses every #721 safety net.
271
284
 
272
- **Exit codes are the no-op rule.** `0` = applied (or a clean no-op). `1` = input error: the
273
- sidecar is absent, carries a malformed line, or holds no records — the store and the archive
274
- were **not touched**;
275
- re-write the sidecar and re-run. `2` = the prune itself failed inside the lib. On any non-zero
276
- exit, surface the error and stop — never retry with a shell rewrite, and never delete `$NEXT`
277
- (the `&&` above already withholds the `rm`, so the assembled generation survives for a retry).
285
+ **Exit codes are the no-op rule.** `0` = applied (or a clean no-op). On `1` and `2`, surface
286
+ the error and stop. `1` = input error: the sidecar is absent, carries a malformed line, holds
287
+ no records, lacks its line-1 `_store_generation` header, or was snapshotted from another store
288
+ than this call's `--file` — the store and the archive were **not touched**. `2` = the prune
289
+ itself failed inside the lib. `3` =
290
+ `store-generation-mismatch`: the store changed after step 1 and nothing was written. This one
291
+ is a retry, not a stop: snapshot into a FRESH path
292
+ (`--prune --snapshot .orchestrator/tmp/learnings-next-retry.jsonl`, with the same `--file` /
293
+ `--archive` as the apply), re-apply this run's
294
+ decisions to the records in THAT file — they now include the peer's change — and run the block
295
+ above with its `NEXT` set to the fresh path. Retry once: a second consecutive `3` means the
296
+ store keeps moving under you — stop and report it. Never move the fresh header onto the old
297
+ sidecar's records: they lack exactly the peer change the header exists to protect, so the
298
+ prune would archive it `pruned` (the error prints no token for that reason). On every exit,
299
+ never retry with a shell rewrite and never delete `$NEXT` — the `&&` above withholds the `rm`,
300
+ so the assembled generation survives as the record of this run's decisions.
278
301
 
279
302
  `pruneLearnings()` — the function the subcommand calls — performs steps 6 + 7 + 8 mechanically
280
303
  and archives **every** record that
@@ -301,21 +324,22 @@ For confirmed learnings, use atomic rewrite strategy:
301
324
  old "read back the first line to confirm valid JSON" check is redundant here: `rewriteLearnings()`
302
325
  round-trip-validates EVERY line before any byte reaches disk (#662), and the `malformed` guard
303
326
  above rejects an unparseable sidecar before the store is touched at all.
304
- 6. **Vault mirror (conditional):** Check `$CONFIG."vault-integration".enabled` via jq. If the field is missing or `false`, skip this step entirely — skill behavior is unchanged.
327
+ 6. **Vault mirror (conditional):** Check `$CONFIG."vault-integration".enabled` via jq. If `$CONFIG."vault-integration"."host-override"` is set and the integration is off (`enabled` is `false` or `mode` is `off`), report `vault-mirror: bewusst aus auf diesem Host (<host-override>)` and skip this step — the host switched it off on purpose (SO#1448), so this is never a failure, even when the committed mode is `strict`. Otherwise, if the field is missing or `false`, skip this step entirely — skill behavior is unchanged.
305
328
 
306
329
  If `enabled` is `true`:
307
330
 
308
331
  a. Check `$CONFIG."vault-integration".mode`. If `mode` is `off`, skip the mirror invocation (treat as disabled). If `mode` is absent, default to `warn`.
309
332
 
310
- b. Resolve the vault directory: use `$CONFIG."vault-integration"."vault-dir"` if non-null, otherwise fall back to the `$VAULT_DIR` environment variable. If neither is set, emit a warning and skip.
333
+ b. Resolve the vault directory: use `$CONFIG."vault-integration"."vault-dir"` if non-null, otherwise fall back to the `$VAULT_DIR` environment variable. If neither is set, emit a warning and skip. Report `vault-dir=<path> (Quelle: <source>)`, where `<source>` is `$CONFIG."vault-integration"."vault-dir-source"` (`env`, `match`, `owner`, `committed`), or `env VAULT_DIR` when the fallback was used.
311
334
 
312
- c. Invoke the mirror script. Derive a synthetic `EVOLVE_SESSION_ID` so the vault-mirror auto-commit phase (#31) produces a traceable commit subject (`chore(vault): mirror evolve-<date> — N learnings + 0 sessions`). Pass `--vault-name` when `vault-integration.vault-name` is set in Session Config:
335
+ c. Invoke the mirror script from the repo root, by its plugin path — `--repo-root "$PWD"` plus an absolute `--source` make the vault namespace this repo's identity, and a ledger from a different repo is refused (exit 2, `source-repo-mismatch`, #1503) instead of overwriting another project's notes. Derive a synthetic `EVOLVE_SESSION_ID` so the vault-mirror auto-commit phase (#31) produces a traceable commit subject (`chore(vault): mirror evolve-<date> — N learnings + 0 sessions`). Pass `--vault-name` when `vault-integration.vault-name` is set in Session Config:
313
336
  ```bash
314
337
  EVOLVE_SESSION_ID="evolve-$(date -u +%Y-%m-%d-%H%M)"
315
338
  EVOLVE_VAULT_NAME=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-name" // empty')
316
339
  node "$PLUGIN_ROOT/scripts/vault-mirror.mjs" \
317
340
  --vault-dir "<vault-dir>" \
318
- --source .orchestrator/metrics/learnings.jsonl \
341
+ --repo-root "$PWD" \
342
+ --source "$PWD/.orchestrator/metrics/learnings.jsonl" \
319
343
  --kind learning \
320
344
  --session-id "$EVOLVE_SESSION_ID" \
321
345
  ${EVOLVE_VAULT_NAME:+--vault-name "$EVOLVE_VAULT_NAME"}
@@ -25,7 +25,7 @@ description: >
25
25
 
26
26
  The user invoked `/memory-cleanup` with arguments: **$ARGUMENTS**. Parse them before anything else — two optional, mutually-exclusive flags are recognised (`--dry-run`, `--apply-pending`, see PRD #502); passing both is an error, and the absence of both selects the legacy interactive 4-phase mode. The per-flag behaviour, the exact status lines and the exit codes are in § Argument Handling (Phase 0) below; the interactive default runs Phases 1-4 (Orient → Gather Signal → Consolidate → Prune & Index) against `~/.claude/projects/<encoded-cwd>/memory/` and reports per § Output.
27
27
 
28
- Sidecar producer/consumer contract: session-end Phase 3.6.5 (`scripts/lib/auto-dream.mjs`) is nudge-only (#614) — it never dispatches a subagent to write the sidecar. The only real producer of `.orchestrator/pending-dream.md` is a manual `/memory-cleanup --dry-run` run; `--apply-pending` is the operator-confirmed consumer in a later session. The sidecar file is single-writer — concurrent sessions cannot collide because the writer holds the session-lock. <!-- path-check: example -->
28
+ Sidecar producer/consumer contract: session-start's `maintenance-due` probe uses `scripts/lib/auto-dream.mjs` to report when cleanup is due; the former session-end Phase 3.6.5 nudge was retired in #1288. Neither path generates the sidecar. The only real producer of `.orchestrator/pending-dream.md` is a manual `/memory-cleanup --dry-run` run; `--apply-pending` is the operator-confirmed consumer in a later session. The sidecar file is single-writer — concurrent sessions cannot collide because the writer holds the session-lock. <!-- path-check: example -->
29
29
 
30
30
  Implements the 4-phase memory consolidation process modelled after Claude Code's Auto Dream feature. Run after major refactors, framework migrations, or every 5+ sessions in a repo.
31
31
 
@@ -184,8 +184,8 @@ Dispatch one Agent per persona from the active catalog set.
184
184
 
185
185
  **Model selection per persona:**
186
186
  - If `persona.model` is a full Claude model ID (`MODEL_ID_RE`): use it as-is.
187
- - If `persona.model` is `opus` or unset AND `persona.tier == 'domain-expert'`: override to
188
- `claude-opus-4-7` (empirically validated — Opus finds real problems Sonnet misses; see vault
187
+ - If `persona.model` is `opus` or unset AND `persona.tier == 'domain-expert'`: use the alias
188
+ `opus`, which tracks the current Opus — pin a full ID only for a named reproducibility need (empirically validated — Opus finds real problems Sonnet misses; see vault
189
189
  learning `[[persona-opus-finds-real-failing-cibadge]]`).
190
190
  - Otherwise: use the persona's declared model alias.
191
191
 
@@ -28,7 +28,7 @@ name: ai-expert
28
28
  schema_version: 1
29
29
  version: "2"
30
30
  role: "AI/ML domain expert — evaluates technical accuracy and implementation quality"
31
- model: claude-opus-4-7
31
+ model: opus
32
32
  tier: domain-expert
33
33
  output_contract:
34
34
  type: object
@@ -59,7 +59,7 @@ evaluation_criteria:
59
59
  | `schema_version` | integer | Must be `1`. |
60
60
  | `version` | string | Non-empty string. Persona content version. Increment on any output-affecting change. Used in sidecar + trend-tracking (#459). |
61
61
  | `role` | string | Identity statement. Injected verbatim as prompt opener. Keep under 200 chars. |
62
- | `model` | string | Full model ID (`MODEL_ID_RE`) or alias (`inherit|sonnet|opus|haiku`). Validated at load time. Recommend `claude-opus-4-7` for `domain-expert` and `compliance` tiers (Opus finds real issues Sonnet misses — vault learning `[[persona-opus-finds-real-failing-cibadge]]`). |
62
+ | `model` | string | Full model ID (`MODEL_ID_RE`) or alias (`inherit|sonnet|opus|haiku`). Validated at load time. Recommend the Opus tier via alias `opus` for `domain-expert` and `compliance` tiers (Opus finds real issues Sonnet misses — vault learning `[[persona-opus-finds-real-failing-cibadge]]`); the alias tracks the current Opus; pin a full model ID only for a named reproducibility need, and name the reason. |
63
63
  | `output_contract` | object | Inline JSON Schema Draft 2020-12. `$ref/$defs/allOf/anyOf` FORBIDDEN (H3). Must require `verdict` + `rationale`. AJV compile wrapped in 2s AbortSignal timeout. |
64
64
  | `evaluation_criteria` | array | Non-empty. Each string max 512 chars. Injected wrapped in `<persona-criteria>` delimiters (M1). Write as statements, not questions. |
65
65
  | `tier` | enum | `domain-expert` \| `buyer-persona` \| `auditor` \| `compliance` \| `reviewer` \| `custom`. Affects model selection (Phase 3). |
@@ -3,7 +3,7 @@ name: designer-lens
3
3
  schema_version: 1
4
4
  version: "1"
5
5
  role: "Designer lens — evaluates interaction flow, cognitive load, consistency, and accessibility"
6
- model: claude-opus-4-7
6
+ model: opus
7
7
  tier: domain-expert
8
8
  evaluation_criteria:
9
9
  - "The interaction flow is legible on first encounter — a new user can predict what happens next without hidden state."
@@ -3,7 +3,7 @@ name: engineer-lens
3
3
  schema_version: 1
4
4
  version: "1"
5
5
  role: "Engineer lens — evaluates feasibility, complexity, failure modes, and long-term maintainability"
6
- model: claude-opus-4-7
6
+ model: opus
7
7
  tier: domain-expert
8
8
  evaluation_criteria:
9
9
  - "Failure modes are enumerated — what breaks under load, bad input, or partial failure, and how the system responds."
@@ -3,7 +3,7 @@ name: pm-lens
3
3
  schema_version: 1
4
4
  version: "1"
5
5
  role: "Product Manager lens — evaluates user value, outcome-metric clarity, and scope discipline"
6
- model: claude-opus-4-7
6
+ model: opus
7
7
  tier: domain-expert
8
8
  evaluation_criteria:
9
9
  - "The change is tied to a specific, named user or business problem — not a solution in search of one."