session-orchestrator 5.6.0 → 5.8.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 (207) 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 +75 -0
  42. package/README.md +10 -10
  43. package/agents/eval-judge.md +5 -5
  44. package/docs/USER-GUIDE.md +1 -1
  45. package/docs/baseline.md +4 -3
  46. package/docs/codex-setup.md +6 -4
  47. package/docs/components.md +5 -4
  48. package/docs/events-schema.md +4 -3
  49. package/docs/github-mirror-protection.md +3 -2
  50. package/docs/install.md +39 -3
  51. package/docs/pi-setup.md +1 -1
  52. package/docs/rule-authoring.md +12 -2
  53. package/docs/session-config-reference.md +8 -6
  54. package/docs/session-config-template.md +4 -3
  55. package/evals/README.md +60 -0
  56. package/evals/instruction-ablation/README.md +147 -0
  57. package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
  58. package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
  59. package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
  60. package/evals/instruction-ablation/run.mjs +336 -0
  61. package/evals/psa-003-foreign-work/case.yaml +16 -0
  62. package/evals/psa-003-foreign-work/fixture.sh +12 -0
  63. package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
  64. package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
  65. package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
  66. package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
  67. package/evals/psa-003-foreign-work/prompt.md +6 -0
  68. package/hooks/_lib/hook-import-set.json +10 -2
  69. package/hooks/hooks-codex.json +1 -1
  70. package/hooks/hooks.json +1 -1
  71. package/hooks/post-tool-batch-wave-signal.mjs +1 -0
  72. package/hooks/post-tooluse-frontend-slop.mjs +11 -1
  73. package/hooks/pre-task-scope-disjoint.mjs +8 -6
  74. package/hooks/wave-scope-commit-guard.mjs +41 -10
  75. package/monitors/monitors.json +1 -1
  76. package/package.json +3 -1
  77. package/pi/prompts/architecture.md +11 -0
  78. package/pi/prompts/eval.md +1 -1
  79. package/pi/prompts/tmux-layout.md +11 -0
  80. package/pi/prompts/write-executable-plan.md +11 -0
  81. package/scripts/backfill-abandoned-sessions.mjs +6 -8
  82. package/scripts/backfill-learnings-from-vault.mjs +17 -6
  83. package/scripts/backfill-sessions.mjs +9 -7
  84. package/scripts/codex-install.mjs +108 -23
  85. package/scripts/emit-session.mjs +15 -2
  86. package/scripts/fleet-auflagen.mjs +54 -0
  87. package/scripts/generate-agents-skills.mjs +9 -16
  88. package/scripts/generate-codex-skills.mjs +8 -6
  89. package/scripts/generate-cursor-adapter.mjs +9 -74
  90. package/scripts/generate-pi-prompts.mjs +8 -74
  91. package/scripts/github-protection-audit.mjs +8 -0
  92. package/scripts/lib/auto-dialectic.mjs +10 -9
  93. package/scripts/lib/auto-dream.mjs +8 -10
  94. package/scripts/lib/autopilot/telemetry.mjs +12 -6
  95. package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
  96. package/scripts/lib/config/drift-check.mjs +1 -1
  97. package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
  98. package/scripts/lib/config.mjs +5 -1
  99. package/scripts/lib/convergence-monitor.mjs +5 -3
  100. package/scripts/lib/dispatcher/rank.mjs +5 -1
  101. package/scripts/lib/ecosystem-health.mjs +5 -3
  102. package/scripts/lib/eval/engine.mjs +61 -27
  103. package/scripts/lib/eval/judge.mjs +14 -8
  104. package/scripts/lib/eval/session-resolve.mjs +7 -2
  105. package/scripts/lib/events-retention-banner.mjs +5 -2
  106. package/scripts/lib/events.mjs +81 -54
  107. package/scripts/lib/fleet-protocol.mjs +98 -1
  108. package/scripts/lib/instruction-budget-guard.mjs +25 -18
  109. package/scripts/lib/io.mjs +181 -39
  110. package/scripts/lib/learnings/candidates.mjs +14 -44
  111. package/scripts/lib/learnings/io.mjs +7 -2
  112. package/scripts/lib/learnings/judgment.mjs +5 -8
  113. package/scripts/lib/learnings/select.mjs +3 -3
  114. package/scripts/lib/maintenance-due-banner.mjs +2 -2
  115. package/scripts/lib/memory-proposals/sink.mjs +5 -6
  116. package/scripts/lib/memory-proposals/store.mjs +12 -7
  117. package/scripts/lib/mirror-issues-banner.mjs +11 -1
  118. package/scripts/lib/native-command.mjs +48 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +29 -3
  120. package/scripts/lib/peer-cards/writer.mjs +6 -18
  121. package/scripts/lib/platform.mjs +113 -4
  122. package/scripts/lib/plugin-package-stage.mjs +169 -0
  123. package/scripts/lib/plugin-surface.mjs +121 -0
  124. package/scripts/lib/quality-gate.mjs +30 -25
  125. package/scripts/lib/reconcile/idempotency.mjs +9 -10
  126. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +97 -8
  127. package/scripts/lib/reconcile/writer.mjs +11 -17
  128. package/scripts/lib/rule-loader.mjs +32 -6
  129. package/scripts/lib/scope-echo.mjs +12 -2
  130. package/scripts/lib/scope-gate.mjs +2 -1
  131. package/scripts/lib/session-close-backfill.mjs +21 -4
  132. package/scripts/lib/session-id.mjs +2 -1
  133. package/scripts/lib/session-record-repair.mjs +9 -13
  134. package/scripts/lib/session-schema/constants.mjs +20 -5
  135. package/scripts/lib/session-start-probes.mjs +2 -2
  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 +12 -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 +585 -72
  156. package/scripts/lib/vault-mirror/prune.mjs +413 -0
  157. package/scripts/lib/vault-mirror/render-sessions.mjs +13 -5
  158. package/scripts/lib/vault-mirror/retention.mjs +388 -0
  159. package/scripts/lib/vault-mirror/utils.mjs +16 -1
  160. package/scripts/lib/vault-status/board-writer.mjs +6 -6
  161. package/scripts/lib/vault-sync-baseline.mjs +9 -8
  162. package/scripts/lib/wave-transcript-tail.mjs +19 -6
  163. package/scripts/lib/worktree/meta.mjs +7 -3
  164. package/scripts/mcp-server.mjs +158 -0
  165. package/scripts/migrate-sessions-jsonl.mjs +7 -5
  166. package/scripts/migrate-vault-paths.mjs +7 -4
  167. package/scripts/print-applicable-rules.mjs +1 -25
  168. package/scripts/print-learnings-index.mjs +2 -1
  169. package/scripts/release.mjs +4 -0
  170. package/scripts/relocate-vault-corpus.mjs +5 -3
  171. package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
  172. package/scripts/self-update.mjs +179 -39
  173. package/scripts/site-numbers.mjs +4 -3
  174. package/scripts/sweep-expired-learnings.mjs +6 -4
  175. package/scripts/sync-vault-schema.mjs +3 -7
  176. package/scripts/vault-mirror-prune.mjs +145 -0
  177. package/scripts/vault-mirror.mjs +77 -12
  178. package/scripts/windows-setup.mjs +45 -0
  179. package/skills/_shared/fleet-protocol.md +8 -0
  180. package/skills/architecture/SKILL.md +1 -0
  181. package/skills/bootstrap/_shared-template.md +5 -2
  182. package/skills/claude-md-drift-check/SKILL.md +4 -1
  183. package/skills/claude-md-drift-check/checker.mjs +89 -26
  184. package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
  185. package/skills/eval/SKILL.md +12 -13
  186. package/skills/eval/rubric-v3.md +521 -0
  187. package/skills/frontmatter-guard/SKILL.md +1 -0
  188. package/skills/gitlab-portfolio/SKILL.md +1 -0
  189. package/skills/hook-development/SKILL.md +1 -0
  190. package/skills/mcp-builder/SKILL.md +1 -0
  191. package/skills/repo-audit/SKILL.md +2 -2
  192. package/skills/session-end/SKILL.md +32 -1
  193. package/skills/session-end/vault-operations.md +12 -2
  194. package/skills/session-start/SKILL.md +2 -2
  195. package/skills/session-start/phase-7-1-premise-check.md +2 -1
  196. package/skills/session-start/references/operations-contract.md +4 -1
  197. package/skills/tmux-layout/SKILL.md +1 -0
  198. package/skills/vault-mirror/SKILL.md +28 -2
  199. package/skills/vault-sync/SKILL.md +6 -6
  200. package/skills/vault-sync/validator.sh +29 -10
  201. package/skills/wave-executor/references/wave-loop-scope-manifest.md +5 -3
  202. package/skills/write-executable-plan/SKILL.md +1 -0
  203. package/hooks/agent-teams-h3-test.sh +0 -362
  204. package/scripts/spikes/h3-agent-teams/preflight.sh +0 -53
  205. package/scripts/spikes/h3-agent-teams/run-h3.sh +0 -112
  206. package/scripts/spikes/h3-agent-teams/setup.sh +0 -137
  207. package/scripts/spikes/h3-agent-teams/toggle.sh +0 -38
@@ -178,8 +178,9 @@ export function countSkills(root) {
178
178
  }
179
179
 
180
180
  /**
181
- * The product's "slash commands" number: `commands/*.md` ∪ skills carrying an
182
- * EXPLICIT `user-invocable: true`, deduplicated by name.
181
+ * The product's "slash commands" number: `commands/*.md` ∪ skills whose
182
+ * `user-invocable` is `true` or ABSENT (Claude Code lists a keyless skill in the
183
+ * `/` picker — #1515), deduplicated by name.
183
184
  *
184
185
  * NOT `ls commands/*.md | wc -l` any more, and the change is not cosmetic. After
185
186
  * the 2026-09-16 fold (#1370) `commands/` holds two files — `session` and
@@ -435,7 +436,7 @@ export const METRIC_DEFS = Object.freeze([
435
436
  id: 'commands',
436
437
  provenance: false,
437
438
  source:
438
- 'ls commands/*.md | wc -l + grep -l "^user-invocable: true" skills/*/SKILL.md | wc -l (union by name — see countCommands)',
439
+ 'ls commands/*.md | wc -l + grep -L "^user-invocable: *false" skills/*/SKILL.md | wc -l (union by name; a skill without the key counts, as in Claude Code — see countCommands)',
439
440
  compute: (root) => fmtCount(countCommands(root)),
440
441
  },
441
442
  {
@@ -123,7 +123,7 @@
123
123
  */
124
124
 
125
125
  import { existsSync, realpathSync, statSync } from 'node:fs';
126
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
126
+ import { mkdir, readFile } from 'node:fs/promises';
127
127
  import path from 'node:path';
128
128
  import { isDeepStrictEqual } from 'node:util';
129
129
  import {
@@ -135,6 +135,7 @@ import {
135
135
  warnUnparseableLines,
136
136
  } from './lib/learnings/expiry-sweep.mjs';
137
137
  import { readLearningsSnapshot, parseLearningsText } from './lib/learnings/io.mjs';
138
+ import { atomicWriteText, envelopeToError } from './lib/io.mjs';
138
139
  import { validateLearning } from './lib/learnings/schema.mjs';
139
140
  import { validatePathInsideProject } from './lib/path-utils.mjs';
140
141
  import { emitEvolveCompleted } from './lib/learnings/evolve-telemetry.mjs';
@@ -668,9 +669,10 @@ async function runSnapshot(args) {
668
669
  ...snap.entries.map((e) => JSON.stringify(e)),
669
670
  ];
670
671
  await mkdir(path.dirname(args.snapshot), { recursive: true });
671
- const tmp = `${args.snapshot}.tmp-${process.pid}`;
672
- await writeFile(tmp, lines.join('\n') + '\n', 'utf8');
673
- await rename(tmp, args.snapshot);
672
+ const res = await atomicWriteText(args.snapshot, lines.join('\n') + '\n', {
673
+ tmpPrefix: `.${path.basename(args.snapshot)}.tmp`,
674
+ });
675
+ if (!res.ok) throw envelopeToError(res);
674
676
  } catch (err) {
675
677
  process.stderr.write(`sweep-expired-learnings: snapshot failed: ${err.message}\n`);
676
678
  process.exit(2);
@@ -26,13 +26,9 @@
26
26
  * 2 — missing file
27
27
  * 3 — malformed sentinels (only one of begin/end present)
28
28
  *
29
- * Vendor-ahead state (2026-05-23, #503, I5):
30
- * The vaultNoteTypeSchema enum in validator.mjs currently includes
31
- * `peer-card` which is NOT yet present in the canonical
32
- * projects-baseline source. Until #503 lands upstream, `--check` will
33
- * report drift; this is expected and is tracked as upstream-sync-debt
34
- * in the #503 PRD (W5 F2 docs agent owns the PRD update). Once
35
- * upstream catches up, `--write` will become a no-op (idempotent).
29
+ * The #503 vendor-ahead state (`peer-card` in validator.mjs before the
30
+ * canonical source carried it) is resolved: projects-baseline has
31
+ * `peer-card`, and `--check` passed against baseline `9108018` (2026-10-04).
36
32
  */
37
33
 
38
34
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * vault-mirror-prune.mjs — CLI for the mirror-zone retention prune (#1513).
4
+ *
5
+ * Applies the vault-mirror retention rules to the notes the mirror wrote
6
+ * BEFORE those rules existed: expired / superseded / duplicate learnings,
7
+ * metrics-only session notes (rolled up first), namespace-alias folders and
8
+ * flat legacy notes. All decisions live in `scripts/lib/vault-mirror/prune.mjs`
9
+ * (rules table in its header) and `retention.mjs`; this file is argv parsing,
10
+ * the manifest, output and exit codes.
11
+ *
12
+ * SAFETY: dry run is the DEFAULT and writes nothing — not even the manifest
13
+ * unless `--manifest` names a path. `--apply` moves files (archive → under
14
+ * `90-archive/mirror/`), never deletes a note's content, never touches a note
15
+ * without the mirror's `_generator` marker.
16
+ *
17
+ * Usage:
18
+ * node scripts/vault-mirror-prune.mjs --vault-dir PATH [--apply]
19
+ * [--manifest PATH] [--now ISO] [--min-narrative-chars N]
20
+ * [--alias FROM=TO ...] [--json]
21
+ *
22
+ * Exit codes: 0 success · 1 usage error · 2 apply finished with per-path errors
23
+ */
24
+
25
+ import { writeFileSync } from 'node:fs';
26
+ import { resolve } from 'node:path';
27
+
28
+ import { expandTilde, redactHomeDir } from './lib/common.mjs';
29
+ import { isMainModule } from './lib/is-main-module.mjs';
30
+ import { MANIFEST_SCHEMA, applyPrune, planPrune } from './lib/vault-mirror/prune.mjs';
31
+
32
+ const HELP = `Usage: node scripts/vault-mirror-prune.mjs --vault-dir PATH [--apply] [--manifest PATH]
33
+ [--now ISO] [--min-narrative-chars N] [--alias FROM=TO ...] [--json]
34
+
35
+ Plans (default) or applies the vault-mirror retention rules to 40-learnings/
36
+ and 50-sessions/ of a vault. Only notes carrying the vault-mirror _generator
37
+ marker are considered; hand-written notes are never touched.
38
+
39
+ Options:
40
+ --vault-dir PATH Vault root (required; no host-local default on purpose)
41
+ --apply Write: rollups, then archive moves to 90-archive/mirror/, then renames
42
+ --manifest PATH Write the JSON manifest (path, reason, action, target) there
43
+ --now ISO Injected clock for the expiry check (default: now)
44
+ --min-narrative-chars N Session narrative gate (default 400, as vault-mirror.quality)
45
+ --alias FROM=TO Explicit namespace alias (repeatable); the hyphen rule applies without it
46
+ --json Print the summary as one JSON line
47
+
48
+ Exit codes: 0 success 1 usage error 2 apply finished with per-path errors
49
+ `;
50
+
51
+ function usageError(msg) {
52
+ process.stderr.write(`vault-mirror-prune: ${msg}\n`);
53
+ process.exit(1);
54
+ }
55
+
56
+ export function parseArgs(argv) {
57
+ const args = { vaultDir: null, apply: false, manifest: null, now: null, minNarrativeChars: 400, aliases: {}, json: false };
58
+ for (let i = 0; i < argv.length; i += 1) {
59
+ const a = argv[i];
60
+ const value = () => {
61
+ const v = argv[i + 1];
62
+ if (v === undefined || v.startsWith('--')) usageError(`${a} needs a value`);
63
+ i += 1;
64
+ return v;
65
+ };
66
+ if (a === '--help' || a === '-h') {
67
+ process.stdout.write(HELP);
68
+ process.exit(0);
69
+ } else if (a === '--vault-dir') args.vaultDir = value();
70
+ else if (a === '--apply') args.apply = true;
71
+ else if (a === '--dry-run') args.apply = false;
72
+ else if (a === '--manifest') args.manifest = value();
73
+ else if (a === '--now') {
74
+ args.now = value();
75
+ if (!Number.isFinite(Date.parse(args.now))) usageError(`--now is not a date: ${args.now}`);
76
+ } else if (a === '--min-narrative-chars') {
77
+ const raw = value();
78
+ if (!/^\d+$/.test(raw)) usageError(`--min-narrative-chars must be a non-negative integer, got ${raw}`);
79
+ args.minNarrativeChars = Number(raw);
80
+ } else if (a === '--alias') {
81
+ const m = /^([a-z0-9][a-z0-9-]*)=([a-z0-9][a-z0-9-]*)$/.exec(value());
82
+ if (!m) usageError('--alias expects FROM=TO with lowercase kebab namespaces');
83
+ args.aliases[m[1]] = m[2];
84
+ } else if (a === '--json') args.json = true;
85
+ else usageError(`unknown argument ${a}`);
86
+ }
87
+ if (!args.vaultDir) usageError('--vault-dir is required');
88
+ return args;
89
+ }
90
+
91
+ export function main(argv = process.argv.slice(2)) {
92
+ const args = parseArgs(argv);
93
+ const vaultDir = resolve(expandTilde(args.vaultDir));
94
+ const today = new Date(args.now ? Date.parse(args.now) : Date.now()).toISOString().slice(0, 10);
95
+ const plan = planPrune({ vaultDir, today, minNarrativeChars: args.minNarrativeChars, aliases: args.aliases });
96
+
97
+ const manifest = {
98
+ schema: MANIFEST_SCHEMA,
99
+ generated_at: new Date().toISOString(),
100
+ today,
101
+ dry_run: !args.apply,
102
+ min_narrative_chars: args.minNarrativeChars,
103
+ aliases: plan.aliases,
104
+ skipped_handwritten: plan.handwritten,
105
+ counts: plan.counts,
106
+ rollups: Object.fromEntries(Object.entries(plan.rollups).map(([k, rows]) => [k, rows.length])),
107
+ actions: plan.actions,
108
+ };
109
+ let applied = null;
110
+ if (args.apply) {
111
+ applied = applyPrune(plan, { vaultDir });
112
+ // The apply result belongs in the manifest: notes written beside an
113
+ // occupied archive target (`suffixed`) and per-path errors.
114
+ manifest.applied = applied;
115
+ }
116
+ if (args.manifest) writeFileSync(resolve(expandTilde(args.manifest)), JSON.stringify(manifest, null, 2) + '\n', 'utf8');
117
+
118
+ const summary = {
119
+ dry_run: !args.apply,
120
+ counts: plan.counts,
121
+ rollups: Object.keys(plan.rollups).length,
122
+ aliases: plan.aliases,
123
+ skipped_handwritten: plan.handwritten,
124
+ ...(applied ? { applied: { ...applied, suffixed: applied.suffixed.length, errors: applied.errors.length } } : {}),
125
+ };
126
+ if (args.json) process.stdout.write(JSON.stringify(summary) + '\n');
127
+ else {
128
+ process.stdout.write(`vault-mirror-prune ${args.apply ? 'APPLY' : 'dry run'} (today ${today})\n`);
129
+ for (const [k, n] of Object.entries(plan.counts).sort()) process.stdout.write(` ${k.padEnd(36)} ${n}\n`);
130
+ process.stdout.write(` rollup notes ${Object.keys(plan.rollups).length}\n`);
131
+ for (const [from, to] of Object.entries(plan.aliases)) process.stdout.write(` alias ${from} -> ${to}\n`);
132
+ if (applied) {
133
+ process.stdout.write(` applied: archived ${applied.archived}, moved ${applied.moved}, rollups written ${applied.rollupsWritten}, suffixed ${applied.suffixed.length}, errors ${applied.errors.length}\n`);
134
+ }
135
+ }
136
+ if (applied && applied.errors.length > 0) {
137
+ for (const e of applied.errors.slice(0, 20)) process.stderr.write(`vault-mirror-prune: ${e.path}: ${redactHomeDir(e.error)}\n`);
138
+ return 2;
139
+ }
140
+ return 0;
141
+ }
142
+
143
+ if (isMainModule(import.meta.url)) {
144
+ process.exit(main());
145
+ }
@@ -20,7 +20,7 @@
20
20
  * `--repo-root` — #1503)
21
21
  *
22
22
  * Output: one JSON line per action on stdout:
23
- * {"action":"created|updated|skipped-noop|skipped-handwritten|skipped-collision-resolved|skipped-foreign-owner|skipped-invalid|skipped-quality-low|skipped-abandoned|skipped-duplicate-session","path":"...","kind":"...","id":"..."}
23
+ * {"action":"created|updated|skipped-noop|skipped-handwritten|skipped-collision-resolved|skipped-foreign-owner|skipped-invalid|skipped-quality-low|skipped-abandoned|skipped-duplicate-session|skipped-metrics-only|skipped-expired|skipped-duplicate-insight","path":"...","kind":"...","id":"..."}
24
24
  *
25
25
  * Idempotency rules:
26
26
  * 1. File does not exist → create.
@@ -45,8 +45,10 @@
45
45
  *
46
46
  * Quality gate (PRD F1.2):
47
47
  * Learnings with confidence < --quality-min-confidence emit `skipped-quality-low`.
48
- * Sessions with rendered-narrative length < --quality-min-narrative-chars emit
49
- * `skipped-quality-low`. Quality gate runs BEFORE --force; --force does NOT
48
+ * Sessions whose free text (`notes`/`narrative`/`summary`) is shorter than
49
+ * --quality-min-narrative-chars get no note: `skipped-metrics-only` with one row
50
+ * in the month rollup `50-sessions/<repo>/_rollup-YYYY-MM.md` (#1513; rules in
51
+ * scripts/lib/vault-mirror/retention.mjs). Quality gate runs BEFORE --force; --force does NOT
50
52
  * bypass the filter. Quality-skipped entries emit `path: null` and an
51
53
  * additional `reason` field describing the violated threshold.
52
54
  *
@@ -65,14 +67,16 @@
65
67
  * Part of session-orchestrator vault-mirror (Issue #14).
66
68
  */
67
69
 
68
- import { existsSync, realpathSync } from 'node:fs';
69
- import { basename, dirname, resolve } from 'node:path';
70
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
71
+ import { basename, dirname, join, resolve } from 'node:path';
70
72
  import { createInterface } from 'node:readline';
71
73
  import { createReadStream } from 'node:fs';
72
74
 
73
75
  import {
74
76
  processLearning,
75
77
  processSession,
78
+ processArchivedLearning,
79
+ learningNoteSlugs,
76
80
  getMaskerStats,
77
81
  emitAction,
78
82
  } from './lib/vault-mirror/process.mjs';
@@ -81,6 +85,7 @@ import { emitEvent } from './lib/events.mjs';
81
85
  import { autoCommitVaultMirror } from './lib/vault-mirror/auto-commit.mjs';
82
86
  import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
83
87
  import { resolveRepoNamespace } from './lib/vault-mirror/namespace.mjs';
88
+ import { SESSION_NARRATIVE_FIELDS, canonicalNamespace } from './lib/vault-mirror/retention.mjs';
84
89
  import { sessionSourceRecord } from './lib/vault-mirror/render-sessions.mjs';
85
90
  import { checkCanonicalVault, describeOriginForLog, findRepoRoot, normalizeRemote } from './lib/named-vault-resolver.mjs';
86
91
  import { loadOwnerConfig } from './lib/owner-yaml.mjs';
@@ -249,8 +254,9 @@ if (flagValues.help === true) {
249
254
  ' project holding --source (<root>/.orchestrator/metrics/), else',
250
255
  ' the git repo containing it, else the cwd. Refused (exit 2,',
251
256
  ' "source-repo-mismatch") when --source lies in a different repo.',
252
- ' --quality-min-narrative-chars <int> Sessions: minimum rendered-narrative length (default 400).',
253
- ' Entries below the threshold emit "skipped-quality-low".',
257
+ ' --quality-min-narrative-chars <int> Sessions: minimum free text in notes/narrative/summary',
258
+ ' (default 400, 0 = off). Below it: "skipped-metrics-only",',
259
+ ' one row in 50-sessions/<repo>/_rollup-YYYY-MM.md.',
254
260
  ' --quality-min-confidence <float> Learnings: minimum confidence threshold (default 0.5).',
255
261
  ' Entries below the threshold emit "skipped-quality-low".',
256
262
  '',
@@ -641,6 +647,9 @@ async function main() {
641
647
  // superseded duplicates included — filled while buffering below, read by
642
648
  // processSession's owner guard (a key from this ledger is never foreign).
643
649
  sourceRecordKeys: new Set(),
650
+ // #1513: note slugs of every LIVE learning in --source; the archive pass
651
+ // below never marks a note one of them still owns.
652
+ liveLearningSlugs: new Set(),
644
653
  qualityMinNarrativeChars,
645
654
  qualityMinConfidence,
646
655
  };
@@ -662,14 +671,15 @@ async function main() {
662
671
  * always supplies a real line number.
663
672
  * @returns {Promise<void>}
664
673
  */
665
- async function dispatchEntry(entry, entryLineNum) {
674
+ async function dispatchEntry(entry, entryLineNum, processor = null) {
666
675
  try {
667
676
  // Both processors return the `action` string they emitted (every one of
668
677
  // their exit paths is an `emitAction` call), so the tally needs no second
669
678
  // census of the 18 call sites in process.mjs — a census that would go
670
679
  // stale the first time a branch is added.
671
- const action =
672
- kind === 'learning'
680
+ const action = processor
681
+ ? await processor(entry, entryLineNum, ctx)
682
+ : kind === 'learning'
673
683
  ? await processLearning(entry, entryLineNum, ctx)
674
684
  : await processSession(entry, entryLineNum, ctx);
675
685
  tally(action);
@@ -776,9 +786,39 @@ async function main() {
776
786
  continue;
777
787
  }
778
788
 
789
+ for (const slug of learningNoteSlugs(entry)) ctx.liveLearningSlugs.add(slug);
779
790
  await dispatchEntry(entry, lineNum);
780
791
  }
781
792
 
793
+ // #1513 ARCHIVE PASS (`--kind learning` only): records /evolve or the expiry
794
+ // sweep moved out of `learnings.jsonl` live on in the sibling
795
+ // `learnings-archive.jsonl`. Their vault notes used to stay `status: draft`
796
+ // forever, because the mirror only ever saw the live store. Each archived
797
+ // record now marks its existing note archived (never creates one). Counted
798
+ // into `total` like any other line, so the run partition still holds. A
799
+ // malformed archive line is reported and skipped — the archive is secondary
800
+ // input and must not abort a mirror run that already wrote the live store.
801
+ if (kind === 'learning' && basename(resolve(source)) === 'learnings.jsonl') {
802
+ const archivePath = join(dirname(resolve(source)), 'learnings-archive.jsonl');
803
+ if (existsSync(archivePath)) {
804
+ const archiveLines = readFileSync(archivePath, 'utf8').split('\n');
805
+ for (let i = 0; i < archiveLines.length; i += 1) {
806
+ const trimmed = archiveLines[i].trim();
807
+ if (!trimmed) continue;
808
+ let archived;
809
+ try {
810
+ archived = JSON.parse(trimmed);
811
+ } catch {
812
+ process.stderr.write(`WARN vault-mirror: malformed JSON on learnings-archive.jsonl line ${i + 1} — skipped\n`);
813
+ continue;
814
+ }
815
+ if (archived === null || typeof archived !== 'object') continue;
816
+ runState.total++;
817
+ await dispatchEntry(archived, i + 1, processArchivedLearning);
818
+ }
819
+ }
820
+ }
821
+
782
822
  if (kind === 'session') {
783
823
  // Only an OBJECT entry carrying a non-empty `session_id` is eligible for
784
824
  // the dedup collapse — the same predicate canonicalizeSessions itself uses
@@ -803,6 +843,30 @@ async function main() {
803
843
  // never a guess (scripts/lib/sessions-canonical.mjs header, "RULE ORDER").
804
844
  const survivors = new Set(canonicalizeSessions(identifiable));
805
845
 
846
+ // #1513 review H2: "newest wins" must not cost a session its narrative. A
847
+ // re-appended record often carries only the metrics; the free text sits on
848
+ // the line it supersedes (vault ledger: 1.172 chars on line 101, none on the
849
+ // winning line 108). The winner inherits every narrative field it LACKS
850
+ // from the losers, latest loser first — its own fields always win.
851
+ const inherited = new Map();
852
+ for (const e of sessionEntries) {
853
+ if (!isIdentifiable(e) || survivors.has(e)) continue;
854
+ const carry = inherited.get(e.session_id) ?? {};
855
+ for (const field of SESSION_NARRATIVE_FIELDS) {
856
+ if (e[field] !== undefined && e[field] !== null && e[field] !== '') carry[field] = e[field];
857
+ }
858
+ inherited.set(e.session_id, carry);
859
+ }
860
+ const withNarrative = (e) => {
861
+ const carry = isIdentifiable(e) ? inherited.get(e.session_id) : undefined;
862
+ if (!carry) return e;
863
+ const merged = { ...e };
864
+ for (const [field, value] of Object.entries(carry)) {
865
+ if (merged[field] === undefined || merged[field] === null || merged[field] === '') merged[field] = value;
866
+ }
867
+ return merged;
868
+ };
869
+
806
870
  for (let i = 0; i < sessionEntries.length; i++) {
807
871
  const entry = sessionEntries[i];
808
872
  if (isIdentifiable(entry) && !survivors.has(entry)) {
@@ -831,7 +895,7 @@ async function main() {
831
895
  );
832
896
  continue;
833
897
  }
834
- await dispatchEntry(entry, sessionLineNums[i]);
898
+ await dispatchEntry(withNarrative(entry), sessionLineNums[i]);
835
899
  }
836
900
  }
837
901
 
@@ -863,7 +927,8 @@ async function main() {
863
927
  autoCommitVaultMirror(
864
928
  resolve(vaultDir),
865
929
  sessionIdArg,
866
- resolveRepoNamespace({ vaultName, repoRoot: ctx.repoRoot }),
930
+ // #1513 review M2: the folder the processors actually wrote into.
931
+ canonicalNamespace(resolve(vaultDir), resolveRepoNamespace({ vaultName, repoRoot: ctx.repoRoot })),
867
932
  );
868
933
  }
869
934
  }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ /** Opt-in native MCP configuration for an installed Windows plugin. */
3
+ import { copyFileSync, existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { isMainModule } from './lib/is-main-module.mjs';
6
+
7
+ export function configureNativeMcp(pluginRoot, { node = process.execPath, platform = process.platform } = {}) {
8
+ if (platform !== 'win32') throw new Error('This setup is for native Windows only.');
9
+ const root = realpathSync(pluginRoot);
10
+ if (existsSync(path.join(root, '.git'))) throw new Error('Refusing to configure a Git checkout.');
11
+ const segments = root.split(path.sep);
12
+ const inCache = segments.some((segment, index) =>
13
+ segment === 'plugins' && segments[index + 1] === 'cache' && index + 2 < segments.length);
14
+ if (!inCache) throw new Error('Expected an installed plugin inside plugins/cache.');
15
+ const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
16
+ if (pkg.name !== 'session-orchestrator') throw new Error('Expected a session-orchestrator plugin directory.');
17
+ const server = path.join(root, 'scripts', 'mcp-server.mjs');
18
+ if (!existsSync(server)) throw new Error('Native MCP server is missing; install the updated package first.');
19
+ const file = path.join(root, '.mcp.json');
20
+ const raw = readFileSync(file, 'utf8');
21
+ const config = JSON.parse(raw);
22
+ const current = config.mcpServers?.['session-orchestrator'];
23
+ if (!current || typeof current !== 'object') throw new Error('Expected session-orchestrator MCP configuration.');
24
+ if (current.command === node && current.args?.length === 1 && current.args[0] === server) {
25
+ return { changed: false, file };
26
+ }
27
+ const backup = `${file}.before-windows-setup`;
28
+ if (existsSync(backup)) throw new Error(`Backup already exists: ${backup}; inspect it before reconfiguring.`);
29
+ config.mcpServers['session-orchestrator'] = { ...current, command: node, args: [server] };
30
+ copyFileSync(file, backup);
31
+ const temporary = `${file}.windows-setup-${process.pid}`;
32
+ writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`, { flag: 'wx' });
33
+ renameSync(temporary, file);
34
+ return { changed: true, file, backup };
35
+ }
36
+
37
+ if (isMainModule(import.meta.url)) {
38
+ try {
39
+ if (process.argv.length !== 3) throw new Error('Usage: node scripts/windows-setup.mjs <installed-plugin-directory>');
40
+ process.stdout.write(`${JSON.stringify(configureNativeMcp(process.argv[2]))}\n`);
41
+ } catch (error) {
42
+ process.stderr.write(`${error.message}\n`);
43
+ process.exitCode = 1;
44
+ }
45
+ }
@@ -103,6 +103,14 @@ Bedingungen, die der Navigator einer Session schreibt (z. B. Agent-Caps, volle P
103
103
  Offload-Regel, Merge- und Zeitregeln, Besitzkonflikte); Inhalt und Form gehören dem Navigator. Die
104
104
  Session **liest** die Datei; sie wartet auf keine Nachricht. Keine Auflage ist eine Freigabe.
105
105
 
106
+ **Pflichtfeld `repo`** (#1520, navigator#190): absoluter Pfad auf Toplevel oder git-common-dir des
107
+ Ziel-Repos. Der Dateiname ist die semantische Session-ID, und die kollidiert über Repos hinweg.
108
+ Gelesen wird über `node scripts/fleet-auflagen.mjs <session_id>` (Code: `readAuflagen()`); die
109
+ Datei gilt nur, wenn `repo` und das eigene Repo auf dasselbe `git rev-parse --path-format=absolute
110
+ --git-common-dir` auflösen, ein Worktree passt also zu seinem Haupt-Checkout. Fehlt das Feld, passt
111
+ es nicht oder ist die Datei unlesbar: fail-closed, WARN auf stderr, die Datei zählt als nicht
112
+ vorhanden, die Standard-Auflagen gelten.
113
+
106
114
  **Standard-Auflagen (konservative Stufe).** Sie gelten ab `fallback_due_at` (stdout der
107
115
  Check-in-CLI), wenn bis dahin keine Auflagen-Datei liegt. Die Frist rechnet `fallbackDueAt()`:
108
116
  `CHECKIN_FALLBACK_MIN` = 10 Minuten nach `checkin.zeit`, solange ein Navigator aktiv ist; ohne
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: architecture
3
3
  description: Use when the user asks to improve architecture, find refactoring opportunities, surface deepening opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable. Surfaces shallow modules and hypothetical seams using a precise vocabulary (Module / Interface / Implementation / Depth / Seam / Adapter / Leverage / Locality from LANGUAGE.md).
4
+ user-invocable: true
4
5
  model: inherit
5
6
  derived-from: mattpocock/skills@90ea8ee
6
7
  license: MIT
@@ -342,7 +342,7 @@ On Codex CLI / Cursor IDE, substitute `.codex/` or `.cursor/` for `.claude/` per
342
342
 
343
343
  Applied by Fast Step 3 (so by every tier), by the Upgrade Flow and by the Refresh-Lock Flow in `SKILL.md` — the last two because a repo bootstrapped before this step existed never gets it otherwise: both flows write no `.gitignore` of their own.
344
344
 
345
- `.orchestrator/` itself stays versioned: `bootstrap.lock`, `policy/`, `steering/`, `peers/` and the `.orchestrator/metrics/*.jsonl` ledgers are durable project data. What the plugin writes there at runtime is per-process state or a per-run artifact that a `git add -A` would otherwise commit — the session lock is rewritten on every SessionStart, every store rewrite leaves a full backup copy of the ledger beside it, the auto-dream sidecar is a full replacement body of the operator's private MEMORY.md, and the quality-gate and ux-grill artifacts can carry secrets or a rendered password. Each pattern below names one writer's file, its `.acquire` guard where it has one, and its own temp files (left behind only by a crash mid-write). Rotated and archived ledger copies (`*.jsonl.[0-9]*`, `*.jsonl.archive-*`, `metrics/_archive/`) are not on this list: they carry ledger history, and whether that is versioned is the consumer's call.
345
+ `.orchestrator/` itself stays versioned: `bootstrap.lock`, `policy/`, `steering/`, `peers/` and the `.orchestrator/metrics/*.jsonl` ledgers are durable project data. What the plugin writes there at runtime is per-process state or a per-run artifact that a `git add -A` would otherwise commit — the session lock is rewritten on every SessionStart, every store rewrite leaves a full backup copy of the ledger beside it, the auto-dream sidecar is a full replacement body of the operator's private MEMORY.md, and the quality-gate and ux-grill artifacts can carry secrets or a rendered password. Each pattern below names one writer's file, its `.acquire` guard where it has one, and its own temp files (left behind only by a crash mid-write). Rotated and archived ledger copies (`*.jsonl.[0-9]*`, `*.jsonl.archive-*`, `metrics/_archive/`) are ignored too (owner decision 2026-10-03, #1507 Pkt 4): the live `*.jsonl` ledger is the versioned record, and the rotated history beside it is not.
346
346
 
347
347
  | Pattern(s) | Written by |
348
348
  |---|---|
@@ -360,7 +360,9 @@ Applied by Fast Step 3 (so by every tier), by the Upgrade Flow and by the Refres
360
360
  | `.orchestrator/staging-fence/` | the staging-fence commit mutex and intent log |
361
361
  | `.orchestrator/tmp/` | machine-local scratch: lock-reaper archives of reaped session locks (`tmp/reaped-locks/`, carrying host name, pid and session ids), worktree metadata (`tmp/worktree-meta/`), the orphan-reaper throttle marker (`tmp/reaper-last-scan`) |
362
362
  | `.orchestrator/metrics/*.jsonl.bak.*` | the legacy evolve backup `learnings.jsonl.bak.evolve-<ts>`, a full store copy still on disk in older repos (`scripts/lib/learnings/io.mjs`) |
363
+ | `.orchestrator/metrics/*.jsonl.[0-9]*`, `.orchestrator/metrics/*.jsonl.archive-*`, `.orchestrator/metrics/_archive/` | rotated ledger history of the events log (`scripts/lib/events.mjs`): the legacy numbered `events.jsonl.<n>` slots, older `*.jsonl.archive-*` copies and the `_archive/` directory of rotated `events-<first>_<last>.jsonl` files (`scripts/lib/events-schema.mjs`); the live `*.jsonl` ledgers stay versioned |
363
364
  | `.orchestrator/metrics/*.jsonl.tmp-*` | atomic-write temp files of the ledgers (`scripts/lib/autopilot/telemetry.mjs`), left behind only by a crash |
365
+ | `.orchestrator/metrics/.*.tmp.*` | temp files of the shared atomic writers (`scripts/lib/io.mjs`, named `.<base>.tmp.<hex>`), left behind only by a crash |
364
366
  | `.orchestrator/pending-dream.md` | the auto-dream sidecar — a full replacement body of the private MEMORY.md (`scripts/lib/auto-dream.mjs`) |
365
367
  | `.orchestrator/dialectic-pending.md`, `.orchestrator/dialectic-last-run`, `.orchestrator/consumed/` | the auto-dialectic sidecar, its last-run timestamp and the archive of consumed sidecars (`scripts/lib/auto-dialectic.mjs`) |
366
368
  | `.orchestrator/promoted-from.json` | the worktree-promotion marker, a per-worktree fact (`scripts/lib/autopilot/worktree-pipeline.mjs`, read by `scripts/lib/session-end/worktree-cleanup.mjs`) |
@@ -396,7 +398,8 @@ _GI_PATTERNS=(
396
398
  '.orchestrator/metrics/proposals-write.lock*' '.orchestrator/metrics/.proposals-write.lock.*'
397
399
  '.orchestrator/staging-fence/'
398
400
  '.orchestrator/tmp/'
399
- '.orchestrator/metrics/*.jsonl.bak.*' '.orchestrator/metrics/*.jsonl.tmp-*'
401
+ '.orchestrator/metrics/*.jsonl.bak.*' '.orchestrator/metrics/*.jsonl.tmp-*' '.orchestrator/metrics/.*.tmp.*'
402
+ '.orchestrator/metrics/*.jsonl.[0-9]*' '.orchestrator/metrics/*.jsonl.archive-*' '.orchestrator/metrics/_archive/'
400
403
  '.orchestrator/pending-dream.md'
401
404
  '.orchestrator/dialectic-pending.md' '.orchestrator/dialectic-last-run' '.orchestrator/consumed/'
402
405
  '.orchestrator/promoted-from.json'
@@ -9,6 +9,7 @@ description: >
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
11
11
  opt-in session-end phase; mirrors vault-sync's lean JSON+exit-code contract.
12
+ user-invocable: false
12
13
  model: haiku
13
14
  ---
14
15
 
@@ -37,7 +38,7 @@ PHASE 1 IMPLEMENTED (2026-04-19). Session-end opt-in quality gate. Upstream of `
37
38
  | 2 | `project-count-sync` | Hardcoded "N registered" / "N projects" claims next to `01-projects/` | compare to `ls -d 01-projects/*/` |
38
39
  | 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
40
  | 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 the name-deduplicated union of `commands/*.md` and skills with explicit `user-invocable: true`; skipped only when both directories are absent |
41
+ | 5 | `command-count` | "N commands" / "N /commands" claims in prose | compare to the name-deduplicated union of `commands/*.md` and skills whose `user-invocable` is `true` or absent (Claude Code picker semantics, #1515); skipped only when both directories are absent |
41
42
  | 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
43
  | 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
44
  | 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 |
@@ -61,6 +62,8 @@ A local CLAUDE.md / AGENTS.md that omits any of these surfaces a `session-config
61
62
 
62
63
  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
64
 
65
+ **Runtime override caveat.** `SO_CONFIG_FILE=AGENTS.md` reads a config-free `@CLAUDE.md` pointer as raw text; `parse-config.mjs` does not follow its import. Check 7 accepting that pointer does not prove an effective runtime config. Set the override to the config-bearing file (for example, `SO_CONFIG_FILE=CLAUDE.md`).
66
+
64
67
  **Alias-by-construction short-circuit.** Since this repo's root `AGENTS.md` became a GENERATED byte-identical copy of `CLAUDE.md` (`scripts/generate-agents-skills.mjs`), Check 7 no longer skips here — it RUNS on every invocation. Two files that cannot disagree have nothing to diff, so the check reports parity as satisfied without deriving one: `instructionFilesAreAliased()` recognises a symlink resolving to the same path, the same inode/device (hardlink or already-resolved symlink), and byte-identical content. Branch order is load-bearing: the "neither file has a `vault-integration:` block" skip is evaluated FIRST, because an absent subject is a different (and more informative) outcome than "the files cannot disagree". The error class the check exists for is untouched — two INDEPENDENT files that declare different `vault-dir` values still produce the `vault-dir-parity` error attributed to `AGENTS.md`.
65
68
 
66
69
  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.