session-orchestrator 5.7.0 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -0
  2. package/.agents/skills/architecture/agents/openai.yaml +5 -0
  3. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -0
  4. package/.agents/skills/eval/SKILL.md +1 -1
  5. package/.agents/skills/frontmatter-guard/SKILL.md +1 -0
  6. package/.agents/skills/gitlab-portfolio/SKILL.md +1 -0
  7. package/.agents/skills/hook-development/SKILL.md +1 -0
  8. package/.agents/skills/mcp-builder/SKILL.md +1 -0
  9. package/.agents/skills/tmux-layout/SKILL.md +3 -0
  10. package/.agents/skills/tmux-layout/agents/openai.yaml +5 -0
  11. package/.agents/skills/vault-mirror/SKILL.md +1 -0
  12. package/.agents/skills/vault-sync/SKILL.md +1 -0
  13. package/.agents/skills/write-executable-plan/SKILL.md +3 -0
  14. package/.agents/skills/write-executable-plan/agents/openai.yaml +5 -0
  15. package/.claude-plugin/marketplace.json +1 -1
  16. package/.claude-plugin/plugin.json +1 -1
  17. package/.codex-plugin/plugin.json +1 -1
  18. package/.codex-plugin/skills/architecture/SKILL.md +3 -0
  19. package/.codex-plugin/skills/architecture/agents/openai.yaml +5 -0
  20. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -0
  21. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  22. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +1 -0
  23. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +1 -0
  24. package/.codex-plugin/skills/hook-development/SKILL.md +1 -0
  25. package/.codex-plugin/skills/mcp-builder/SKILL.md +1 -0
  26. package/.codex-plugin/skills/tmux-layout/SKILL.md +3 -0
  27. package/.codex-plugin/skills/tmux-layout/agents/openai.yaml +5 -0
  28. package/.codex-plugin/skills/vault-mirror/SKILL.md +1 -0
  29. package/.codex-plugin/skills/vault-sync/SKILL.md +1 -0
  30. package/.codex-plugin/skills/write-executable-plan/SKILL.md +3 -0
  31. package/.codex-plugin/skills/write-executable-plan/agents/openai.yaml +5 -0
  32. package/.cursor/commands/architecture.md +13 -0
  33. package/.cursor/commands/eval.md +1 -1
  34. package/.cursor/commands/tmux-layout.md +13 -0
  35. package/.cursor/commands/write-executable-plan.md +13 -0
  36. package/.cursor/skills/architecture/SKILL.md +0 -1
  37. package/.cursor/skills/eval/SKILL.md +1 -1
  38. package/.cursor/skills/tmux-layout/SKILL.md +0 -1
  39. package/.cursor/skills/write-executable-plan/SKILL.md +0 -1
  40. package/.cursor-plugin/plugin.json +1 -1
  41. package/CHANGELOG.md +68 -0
  42. package/README.md +13 -9
  43. package/agents/eval-judge.md +5 -5
  44. package/docs/baseline.md +4 -3
  45. package/docs/codex-setup.md +6 -4
  46. package/docs/components.md +5 -4
  47. package/docs/events-schema.md +45 -4
  48. package/docs/github-mirror-protection.md +3 -2
  49. package/docs/install.md +39 -3
  50. package/docs/pi-setup.md +1 -1
  51. package/docs/rule-authoring.md +2 -2
  52. package/docs/session-config-reference.md +7 -5
  53. package/docs/session-config-template.md +3 -2
  54. package/evals/README.md +60 -0
  55. package/evals/instruction-ablation/README.md +147 -0
  56. package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
  57. package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
  58. package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
  59. package/evals/instruction-ablation/run.mjs +336 -0
  60. package/evals/psa-003-foreign-work/case.yaml +16 -0
  61. package/evals/psa-003-foreign-work/fixture.sh +12 -0
  62. package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
  63. package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
  64. package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
  65. package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
  66. package/evals/psa-003-foreign-work/prompt.md +6 -0
  67. package/hooks/_lib/hook-import-set.json +10 -2
  68. package/hooks/hooks-codex.json +1 -1
  69. package/hooks/hooks.json +1 -1
  70. package/hooks/post-tool-batch-wave-signal.mjs +1 -0
  71. package/hooks/post-tooluse-frontend-slop.mjs +11 -1
  72. package/hooks/pre-task-scope-disjoint.mjs +8 -6
  73. package/monitors/monitors.json +1 -1
  74. package/package.json +3 -1
  75. package/pi/prompts/architecture.md +11 -0
  76. package/pi/prompts/eval.md +1 -1
  77. package/pi/prompts/tmux-layout.md +11 -0
  78. package/pi/prompts/write-executable-plan.md +11 -0
  79. package/scripts/backfill-abandoned-sessions.mjs +2 -2
  80. package/scripts/backfill-sessions.mjs +9 -7
  81. package/scripts/codex-install.mjs +108 -23
  82. package/scripts/emit-session.mjs +15 -2
  83. package/scripts/fleet-auflagen.mjs +54 -0
  84. package/scripts/generate-agents-skills.mjs +9 -16
  85. package/scripts/generate-codex-skills.mjs +8 -6
  86. package/scripts/generate-cursor-adapter.mjs +9 -74
  87. package/scripts/generate-pi-prompts.mjs +8 -74
  88. package/scripts/github-protection-audit.mjs +8 -0
  89. package/scripts/lib/auto-dialectic.mjs +10 -9
  90. package/scripts/lib/auto-dream.mjs +8 -10
  91. package/scripts/lib/autopilot/telemetry.mjs +12 -6
  92. package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
  93. package/scripts/lib/ci-status-banner.mjs +38 -5
  94. package/scripts/lib/config/drift-check.mjs +1 -1
  95. package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
  96. package/scripts/lib/config.mjs +5 -1
  97. package/scripts/lib/convergence-monitor.mjs +5 -3
  98. package/scripts/lib/ecosystem-health.mjs +5 -3
  99. package/scripts/lib/eval/engine.mjs +61 -27
  100. package/scripts/lib/eval/judge.mjs +14 -8
  101. package/scripts/lib/eval/session-resolve.mjs +7 -2
  102. package/scripts/lib/events-retention-banner.mjs +5 -2
  103. package/scripts/lib/events-schema.mjs +8 -0
  104. package/scripts/lib/events.mjs +19 -5
  105. package/scripts/lib/fleet-protocol.mjs +98 -1
  106. package/scripts/lib/instruction-budget-guard.mjs +25 -18
  107. package/scripts/lib/io.mjs +172 -35
  108. package/scripts/lib/learnings/candidates.mjs +7 -28
  109. package/scripts/lib/learnings/io.mjs +7 -2
  110. package/scripts/lib/learnings/judgment.mjs +5 -8
  111. package/scripts/lib/learnings/select.mjs +3 -3
  112. package/scripts/lib/maintenance-due-banner.mjs +2 -2
  113. package/scripts/lib/memory-proposals/sink.mjs +5 -6
  114. package/scripts/lib/memory-proposals/store.mjs +3 -5
  115. package/scripts/lib/mirror-issues-banner.mjs +11 -1
  116. package/scripts/lib/native-command.mjs +48 -0
  117. package/scripts/lib/orphan-reaper/scan.mjs +29 -3
  118. package/scripts/lib/peer-cards/writer.mjs +6 -18
  119. package/scripts/lib/platform.mjs +113 -4
  120. package/scripts/lib/plugin-package-stage.mjs +169 -0
  121. package/scripts/lib/plugin-surface.mjs +121 -0
  122. package/scripts/lib/quality-gate-wave.mjs +40 -0
  123. package/scripts/lib/quality-gate.mjs +36 -30
  124. package/scripts/lib/reconcile/idempotency.mjs +9 -10
  125. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +48 -11
  126. package/scripts/lib/reconcile/writer.mjs +11 -17
  127. package/scripts/lib/rule-loader.mjs +32 -6
  128. package/scripts/lib/scope-echo.mjs +12 -2
  129. package/scripts/lib/scope-gate.mjs +2 -1
  130. package/scripts/lib/session-close-backfill.mjs +3 -4
  131. package/scripts/lib/session-id.mjs +2 -1
  132. package/scripts/lib/session-record-repair.mjs +28 -21
  133. package/scripts/lib/session-schema/constants.mjs +20 -5
  134. package/scripts/lib/session-schema/validator.mjs +12 -0
  135. package/scripts/lib/session-start-probes.mjs +22 -7
  136. package/scripts/lib/sessions-staleness-banner.mjs +2 -3
  137. package/scripts/lib/skill-evolution/engine.mjs +6 -9
  138. package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
  139. package/scripts/lib/slopcheck.mjs +9 -7
  140. package/scripts/lib/sunset/walker.mjs +3 -3
  141. package/scripts/lib/telemetry/sync.mjs +3 -1
  142. package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
  143. package/scripts/lib/test-sibling-coverage.mjs +167 -0
  144. package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
  145. package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
  146. package/scripts/lib/user-invocable-skills.mjs +37 -10
  147. package/scripts/lib/ux-grill/run-record.mjs +5 -3
  148. package/scripts/lib/validate/check-banner-parity.mjs +1 -0
  149. package/scripts/lib/validate/check-skills.mjs +12 -2
  150. package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
  151. package/scripts/lib/validate/check-test-value-bans.mjs +3 -2
  152. package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
  153. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  154. package/scripts/lib/vault-archive.mjs +2 -4
  155. package/scripts/lib/vault-mirror/process.mjs +207 -26
  156. package/scripts/lib/vault-mirror/utils.mjs +16 -1
  157. package/scripts/lib/vault-sync-baseline.mjs +9 -8
  158. package/scripts/lib/wave-executor/remote-dispatch.mjs +27 -9
  159. package/scripts/lib/wave-resource-gate.mjs +2 -1
  160. package/scripts/lib/wave-transcript-tail.mjs +8 -4
  161. package/scripts/lib/worktree/meta.mjs +7 -3
  162. package/scripts/mcp-server.mjs +158 -0
  163. package/scripts/migrate-sessions-jsonl.mjs +7 -5
  164. package/scripts/migrate-vault-paths.mjs +7 -4
  165. package/scripts/print-learnings-index.mjs +2 -1
  166. package/scripts/release.mjs +4 -1
  167. package/scripts/relocate-vault-corpus.mjs +5 -3
  168. package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
  169. package/scripts/run-quality-gate.mjs +9 -39
  170. package/scripts/self-update.mjs +179 -39
  171. package/scripts/site-numbers.mjs +8 -3
  172. package/scripts/sweep-expired-learnings.mjs +6 -4
  173. package/scripts/sync-vault-schema.mjs +3 -7
  174. package/scripts/windows-setup.mjs +45 -0
  175. package/skills/_shared/fleet-protocol.md +10 -0
  176. package/skills/architecture/SKILL.md +1 -0
  177. package/skills/bootstrap/_shared-template.md +5 -2
  178. package/skills/claude-md-drift-check/SKILL.md +4 -1
  179. package/skills/claude-md-drift-check/checker.mjs +83 -10
  180. package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
  181. package/skills/eval/SKILL.md +12 -13
  182. package/skills/eval/rubric-v3.md +521 -0
  183. package/skills/frontmatter-guard/SKILL.md +1 -0
  184. package/skills/gitlab-portfolio/SKILL.md +1 -0
  185. package/skills/hook-development/SKILL.md +1 -0
  186. package/skills/mcp-builder/SKILL.md +1 -0
  187. package/skills/remote-offload/SKILL.md +1 -1
  188. package/skills/repo-audit/SKILL.md +2 -2
  189. package/skills/session-end/SKILL.md +32 -1
  190. package/skills/session-end/metrics-collection.md +15 -9
  191. package/skills/session-end/vault-operations.md +12 -2
  192. package/skills/session-start/SKILL.md +2 -2
  193. package/skills/session-start/phase-7-1-premise-check.md +2 -1
  194. package/skills/session-start/references/operations-contract.md +4 -1
  195. package/skills/tmux-layout/SKILL.md +1 -0
  196. package/skills/vault-mirror/SKILL.md +1 -0
  197. package/skills/vault-sync/SKILL.md +6 -6
  198. package/skills/vault-sync/validator.sh +29 -10
  199. package/skills/wave-executor/references/wave-loop-dispatch.md +1 -1
  200. package/skills/wave-executor/references/wave-loop-review.md +13 -7
  201. package/skills/wave-executor/references/wave-loop-scope-manifest.md +4 -4
  202. package/skills/write-executable-plan/SKILL.md +1 -0
@@ -15,10 +15,19 @@
15
15
  * Steps (each skipped when its harness is not installed on this host):
16
16
  * 1. repo — `git fetch origin` + `git merge --ff-only origin/main`, then
17
17
  * `npm ci` when HEAD moved or node_modules is missing.
18
- * 2. claude — marketplace update + plugin update, then verify version and
19
- * runtime dependencies in the reported installPath.
18
+ * 2. claude — `npm pack` this clone and extract it into STAGE_DIR, then
19
+ * marketplace update + plugin update, then verify version and
20
+ * runtime dependencies in the reported installPath. The
21
+ * marketplace must read STAGE_DIR, not this clone: a directory
22
+ * marketplace on the working checkout copied ALL of it into the
23
+ * plugin cache — 7557 files instead of 1164, `.env.local`
24
+ * (NPM_TOKEN) and `.orchestrator/` included (#1515, measured
25
+ * 2026-10-03). The step refuses, naming the remove + add
26
+ * commands, while ANY directory marketplace reads this clone;
27
+ * GitHub/npm sources update as before, with a one-line hint.
20
28
  * 3. codex — only if the plugin is already installed in Codex: re-run
21
- * scripts/codex-install.mjs (refreshes the installed bundle).
29
+ * scripts/codex-install.mjs, which installs from the same
30
+ * packed STAGE_DIR (#1518), never from this clone.
22
31
  *
23
32
  * Never deletes an old plugin cache directory: the running session still
24
33
  * loads it until the harness restarts.
@@ -26,20 +35,78 @@
26
35
  * Exit codes: 0 all steps ok/skipped · 1 usage error · 2 a step failed.
27
36
  */
28
37
 
29
- import { existsSync, readFileSync } from 'node:fs';
38
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
39
+ import os from 'node:os';
30
40
  import path from 'node:path';
31
- import { spawnSync } from 'node:child_process';
41
+ import { commandOnPath, spawnNativeSync } from './lib/native-command.mjs';
32
42
  import { fileURLToPath } from 'node:url';
33
43
 
34
44
  import { isMainModule } from './lib/is-main-module.mjs';
45
+ import { resolveStageDir, stagePackage } from './lib/plugin-package-stage.mjs';
35
46
 
36
47
  const SO_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
48
+ // Codex install id; the Claude id is read from the installed entry, because
49
+ // the marketplace name is whatever the host registered (`kanevry` per
50
+ // .claude-plugin/marketplace.json, `session-orchestrator` on older hosts).
37
51
  const PLUGIN_ID = 'session-orchestrator@kanevry';
38
- const MARKETPLACE = 'kanevry';
52
+ const PLUGIN_NAME = 'session-orchestrator';
39
53
  const COMMAND_TIMEOUT_MS = 300_000;
40
54
  // `codex plugin list --available --json` is 2.56 MB on a host with several
41
55
  // marketplaces (measured 2026-10-02); the 1 MiB default fails with ENOBUFS.
42
56
  const MAX_BUFFER = 64 * 1024 * 1024;
57
+ /** Host-local copy of the packed package; the marketplace reads this, never the clone. */
58
+ const STAGE_DIR = resolveStageDir();
59
+
60
+ /**
61
+ * Claude Code config dir: CLAUDE_CONFIG_DIR when absolute, else ~/.claude. A
62
+ * relative value is ignored like a relative XDG_CACHE_HOME — resolved against
63
+ * whatever cwd this script runs in, it would point the marketplace guard at a
64
+ * file that does not exist and let the update run unguarded (#1519).
65
+ * @param {NodeJS.ProcessEnv} [env]
66
+ * @param {string} [home]
67
+ * @returns {{dir: string, ignored: string | null}}
68
+ */
69
+ export function resolveClaudeConfigDir(env = process.env, home = os.homedir()) {
70
+ const raw = (env.CLAUDE_CONFIG_DIR || '').trim();
71
+ if (path.isAbsolute(raw)) return { dir: raw, ignored: null };
72
+ return { dir: path.join(home, '.claude'), ignored: raw || null };
73
+ }
74
+
75
+ const CLAUDE_CONFIG = resolveClaudeConfigDir();
76
+ const KNOWN_MARKETPLACES = path.join(CLAUDE_CONFIG.dir, 'plugins', 'known_marketplaces.json');
77
+
78
+ const realOrResolved = (p) => { try { return realpathSync(p); } catch { return path.resolve(p); } };
79
+
80
+ /**
81
+ * Registered Claude Code marketplaces from `known_marketplaces.json`, read
82
+ * defensively: a missing or unparseable file yields `null`, never a throw.
83
+ * @param {string} file
84
+ * @returns {Array<{name: string, source: string | undefined, path: string | undefined}> | null}
85
+ */
86
+ export function readKnownMarketplaces(file) {
87
+ let raw;
88
+ try { raw = JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
89
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
90
+ return Object.entries(raw).map(([name, v]) => ({
91
+ name,
92
+ source: typeof v?.source?.source === 'string' ? v.source.source : undefined,
93
+ path: typeof v?.source?.path === 'string' ? v.source.path : undefined,
94
+ }));
95
+ }
96
+
97
+ /**
98
+ * Names of the `directory` marketplaces that read `dir` (realpath-compared) —
99
+ * whatever they are called.
100
+ * @param {ReturnType<typeof readKnownMarketplaces>} known
101
+ * @param {string} dir
102
+ * @returns {string[]}
103
+ */
104
+ export function directoryMarketplacesOn(known, dir) {
105
+ const target = realOrResolved(dir);
106
+ return (known ?? [])
107
+ .filter((m) => m.source === 'directory' && m.path && realOrResolved(m.path) === target)
108
+ .map((m) => m.name);
109
+ }
43
110
 
44
111
  /**
45
112
  * Pick the session-orchestrator entry from `claude plugin list --json`.
@@ -50,8 +117,9 @@ const MAX_BUFFER = 64 * 1024 * 1024;
50
117
  export function findClaudeEntry(listJson) {
51
118
  const entries = Array.isArray(listJson) ? listJson : listJson?.plugins;
52
119
  if (!Array.isArray(entries)) return null;
53
- return entries.find((e) => e?.id === PLUGIN_ID && e?.scope === 'user')
54
- ?? entries.find((e) => e?.id === PLUGIN_ID)
120
+ const ours = (e) => typeof e?.id === 'string' && e.id.startsWith(`${PLUGIN_NAME}@`);
121
+ return entries.find((e) => ours(e) && e?.scope === 'user')
122
+ ?? entries.find(ours)
55
123
  ?? null;
56
124
  }
57
125
 
@@ -90,16 +158,16 @@ to the newest Session Orchestrator, and verify each one afterwards.
90
158
  --json Print the per-step result as JSON
91
159
  `;
92
160
 
93
- function makeRunner({ dryRun, json }) {
161
+ export function makeRunner({ dryRun, json }) {
94
162
  const log = (line) => { if (!json) process.stdout.write(`${line}\n`); };
95
163
  /** Run a command; read-only queries also run under --dry-run. */
96
- function run(cmd, args, { cwd = SO_ROOT, mutates = true } = {}) {
164
+ function run(cmd, args, { cwd = SO_ROOT, mutates = true, env } = {}) {
97
165
  const shown = `${cmd} ${args.join(' ')}`;
98
166
  if (dryRun && mutates) {
99
167
  log(` [dry-run] ${shown}${cwd === SO_ROOT ? '' : ` (in ${cwd})`}`);
100
168
  return { ok: true, stdout: '' };
101
169
  }
102
- const r = spawnSync(cmd, args, { cwd, encoding: 'utf8', timeout: COMMAND_TIMEOUT_MS, maxBuffer: MAX_BUFFER });
170
+ const r = spawnNativeSync(cmd, args, { cwd, env, encoding: 'utf8', timeout: COMMAND_TIMEOUT_MS, maxBuffer: MAX_BUFFER });
103
171
  if (r.error) return { ok: false, detail: `${shown}: ${r.error.message}` };
104
172
  if (r.status !== 0) {
105
173
  return { ok: false, detail: `${shown} exited ${r.status}: ${(r.stderr || r.stdout || '').trim().slice(-400)}` };
@@ -109,12 +177,8 @@ function makeRunner({ dryRun, json }) {
109
177
  return { run, log };
110
178
  }
111
179
 
112
- function onPath(bin) {
113
- return spawnSync('which', [bin], { encoding: 'utf8' }).status === 0;
114
- }
115
-
116
- function repoVersion() {
117
- return JSON.parse(readFileSync(path.join(SO_ROOT, 'package.json'), 'utf8')).version;
180
+ function repoVersion(root = SO_ROOT) {
181
+ return JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).version;
118
182
  }
119
183
 
120
184
  function updateRepo({ run, log }, opts) {
@@ -139,36 +203,112 @@ function updateRepo({ run, log }, opts) {
139
203
  return { step: 'repo', status: 'ok', detail: `${repoVersion()}${moved ? ' (updated)' : ' (already current)'}` };
140
204
  }
141
205
 
142
- function updateClaude({ run, log }, opts) {
143
- if (!onPath('claude')) return { step: 'claude', status: 'skipped', detail: 'claude not on PATH' };
144
- log('claude: refreshing marketplace and plugin');
145
- for (const args of [['plugin', 'marketplace', 'update', MARKETPLACE], ['plugin', 'update', PLUGIN_ID]]) {
146
- const r = run('claude', args);
147
- if (!r.ok) return { step: 'claude', status: 'failed', detail: r.detail };
206
+ /** Marketplace name the packed copy registers under (its own marketplace.json). */
207
+ function packedMarketplaceName(root) {
208
+ try {
209
+ return JSON.parse(readFileSync(path.join(root, '.claude-plugin', 'marketplace.json'), 'utf8')).name || 'kanevry';
210
+ } catch {
211
+ return 'kanevry';
212
+ }
213
+ }
214
+
215
+ /**
216
+ * The Claude Code step. `ctx` exists for tests (tmp config dir, tmp clone,
217
+ * no PATH lookup); production passes nothing.
218
+ */
219
+ export function updateClaude({ run, log }, opts, ctx = {}) {
220
+ const {
221
+ soRoot = SO_ROOT,
222
+ stageDir = STAGE_DIR,
223
+ knownMarketplacesFile = KNOWN_MARKETPLACES,
224
+ ignoredConfigDir = CLAUDE_CONFIG.ignored,
225
+ hasClaude = () => commandOnPath('claude'),
226
+ } = ctx;
227
+ const fail = (detail) => ({ step: 'claude', status: 'failed', detail });
228
+ if (!hasClaude()) return { step: 'claude', status: 'skipped', detail: 'claude not on PATH' };
229
+ if (ignoredConfigDir) {
230
+ log(`claude: ignoring relative CLAUDE_CONFIG_DIR '${ignoredConfigDir}' — reading ${knownMarketplacesFile}`);
231
+ }
232
+ const stage = () => stagePackage({
233
+ soRoot, stageDir, run, log: (line) => log(`claude: ${line}`), dryRun: opts.dryRun,
234
+ });
235
+
236
+ const parseEntry = (r) => {
237
+ if (!r.ok) return { error: r.detail };
238
+ try { return { entry: findClaudeEntry(JSON.parse(r.stdout)) }; } catch (e) {
239
+ return { error: `plugin list --json unparseable: ${e.message}` };
240
+ }
241
+ };
242
+ // Installed-check first: with nothing installed there is nothing to guard,
243
+ // and the refusal below would replace the host's stage dir for no install (#1519).
244
+ const before = parseEntry(run('claude', ['plugin', 'list', '--json'], { mutates: false }));
245
+ if (before.error) return fail(before.error);
246
+ if (!before.entry) return { step: 'claude', status: 'skipped', detail: `${PLUGIN_NAME} not installed` };
247
+
248
+ const known = readKnownMarketplaces(knownMarketplacesFile);
249
+ const leaking = directoryMarketplacesOn(known, soRoot);
250
+ if (leaking.length > 0) {
251
+ // Stage first so the `add` target named below exists.
252
+ const staged = stage();
253
+ const packedName = packedMarketplaceName(soRoot);
254
+ // A marketplace already holding the packed copy's name (e.g. a GitHub
255
+ // `kanevry`) would make `marketplace add <stage>` fail on the taken name.
256
+ const sameName = known?.find((m) => m.name === packedName);
257
+ const nameTaken = sameName && !leaking.includes(packedName)
258
+ && !(sameName.source === 'directory' && sameName.path && realOrResolved(sameName.path) === realOrResolved(stageDir));
259
+ const removes = [...leaking, ...(nameTaken ? [packedName] : [])]
260
+ .map((n) => `claude plugin marketplace remove ${n}`).join(' && ');
261
+ return fail(
262
+ `directory marketplace ${leaking.map((n) => `'${n}'`).join(', ')} reads this clone — the plugin cache `
263
+ + `becomes a full copy incl. .env.local (#1515). Re-register once: ${removes} && `
264
+ + `claude plugin marketplace add ${stageDir} && claude plugin install ${PLUGIN_NAME}@${packedName}`
265
+ + (staged.ok ? '' : ` (staging failed: ${staged.detail})`),
266
+ );
148
267
  }
149
- const list = run('claude', ['plugin', 'list', '--json'], { mutates: false });
150
- if (!list.ok) return { step: 'claude', status: 'failed', detail: list.detail };
151
- let entry;
152
- try { entry = findClaudeEntry(JSON.parse(list.stdout)); } catch (e) {
153
- return { step: 'claude', status: 'failed', detail: `plugin list --json unparseable: ${e.message}` };
268
+
269
+ const pluginId = before.entry.id;
270
+ const marketplace = pluginId.slice(pluginId.indexOf('@') + 1);
271
+ const reg = known?.find((m) => m.name === marketplace);
272
+ const fromStage = reg?.source === 'directory' && !!reg.path && realOrResolved(reg.path) === realOrResolved(stageDir);
273
+
274
+ if (fromStage) {
275
+ const staged = stage();
276
+ if (!staged.ok) return fail(staged.detail);
277
+ } else {
278
+ log(known
279
+ ? `claude: marketplace '${marketplace}' reads ${reg?.path ?? reg?.source ?? 'an unknown source'}; to install this clone instead, register the packed copy at ${stageDir}`
280
+ : `claude: ${knownMarketplacesFile} unreadable — cannot check the marketplace source; the packed-copy path is ${stageDir}`);
154
281
  }
155
- if (!entry) return { step: 'claude', status: 'skipped', detail: `${PLUGIN_ID} not installed` };
156
- if (!entry.installPath || !existsSync(entry.installPath)) {
157
- return { step: 'claude', status: 'failed', detail: `installPath missing: ${entry.installPath}` };
282
+
283
+ log(`claude: refreshing marketplace '${marketplace}' and plugin ${pluginId}`);
284
+ for (const args of [['plugin', 'marketplace', 'update', marketplace], ['plugin', 'update', pluginId]]) {
285
+ const r = run('claude', args);
286
+ if (!r.ok) return fail(r.detail);
158
287
  }
288
+ const after = parseEntry(run('claude', ['plugin', 'list', '--json'], { mutates: false }));
289
+ if (after.error) return fail(after.error);
290
+ const entry = after.entry;
291
+ if (!entry) return fail(`${pluginId} disappeared from plugin list after the update`);
292
+ if (!entry.installPath || !existsSync(entry.installPath)) return fail(`installPath missing: ${entry.installPath}`);
159
293
  const missing = missingRuntimeDeps(entry.installPath);
160
294
  if (missing.length > 0) {
161
295
  const refused = (entry.noteDetails ?? []).some((n) => n?.type === 'dependencies-refused');
162
296
  log(`claude: ${missing.length} runtime deps missing${refused ? ' (harness refused the install)' : ''} — installing`);
163
- const r = run('npm', ['ci', '--omit=dev'], { cwd: entry.installPath });
164
- if (!r.ok) return { step: 'claude', status: 'failed', detail: r.detail };
297
+ // The packed copy carries no .npmrc, so the repo's ignore-scripts=true
298
+ // (SEC-020) has to travel as a flag.
299
+ const r = run('npm', ['ci', '--omit=dev', '--ignore-scripts'], { cwd: entry.installPath });
300
+ if (!r.ok) return fail(r.detail);
165
301
  const still = opts.dryRun ? [] : missingRuntimeDeps(entry.installPath);
166
- if (still.length > 0) return { step: 'claude', status: 'failed', detail: `still missing after npm ci: ${still.join(', ')}` };
302
+ if (still.length > 0) return fail(`still missing after npm ci: ${still.join(', ')}`);
303
+ }
304
+ const want = repoVersion(soRoot);
305
+ if (entry.version !== want && fromStage && !opts.dryRun) {
306
+ // The source IS this clone's packed copy, so a different version means
307
+ // the update did not take effect — not that a remote is behind.
308
+ return fail(`update did not take effect: installed ${entry.version}, packed ${want}`);
167
309
  }
168
- const want = repoVersion();
169
- // The marketplace tracks GitHub main, so it can trail this clone until the
170
- // release is pushed — report it, do not fail on it.
171
- const lag = entry.version !== want ? `; clone is ${want} — marketplace not yet at it` : '';
310
+ const lag = entry.version !== want && !fromStage
311
+ ? `; clone is ${want} — marketplace '${marketplace}' source is not at it yet` : '';
172
312
  return {
173
313
  step: 'claude',
174
314
  status: 'ok',
@@ -177,7 +317,7 @@ function updateClaude({ run, log }, opts) {
177
317
  }
178
318
 
179
319
  function updateCodex({ run, log }, opts) {
180
- if (!onPath('codex')) return { step: 'codex', status: 'skipped', detail: 'codex not on PATH' };
320
+ if (!commandOnPath('codex')) return { step: 'codex', status: 'skipped', detail: 'codex not on PATH' };
181
321
  const list = run('codex', ['plugin', 'list'], { mutates: false });
182
322
  if (!list.ok) return { step: 'codex', status: 'failed', detail: list.detail };
183
323
  if (!new RegExp(`^${PLUGIN_ID}\\s+installed`, 'm').test(list.stdout)) {
@@ -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
  {
@@ -995,11 +996,15 @@ export const SITEMAP_FILE = 'sitemap.xml';
995
996
  * Unknown either way is an ERROR, never a skip (the `METRIC_IDS` rule): a
996
997
  * sitemap `<loc>` missing here would never be dated, and a page carrying cells
997
998
  * that is missing here would drift unseen.
999
+ *
1000
+ * A page without counted cells (for example /lead) never has its <lastmod>
1001
+ * raised automatically: bump it by hand whenever its content changes.
998
1002
  */
999
1003
  export const SITEMAP_PAGES = Object.freeze([
1000
1004
  { page: 'index.html', loc: 'https://session-orchestrator.com/' },
1001
1005
  { page: 'de/index.html', loc: 'https://session-orchestrator.com/de' },
1002
1006
  { page: 'guide/index.html', loc: 'https://session-orchestrator.com/guide' },
1007
+ { page: 'lead/index.html', loc: 'https://session-orchestrator.com/lead' },
1003
1008
  { page: 'impressum/index.html', loc: 'https://session-orchestrator.com/impressum' },
1004
1009
  { page: 'datenschutz/index.html', loc: 'https://session-orchestrator.com/datenschutz' },
1005
1010
  ]);
@@ -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,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
+ }
@@ -8,6 +8,8 @@ knows paths, fields, deadlines and lease validity; `scripts/fleet-checkin.mjs` w
8
8
  This file describes them and never redefines them. The coordinator itself is not part of this
9
9
  package: it is operated outside the plugin, which owns its operational detail (how it measures,
10
10
  which conditions it issues, how it acquires and hands over the lease).
11
+ A public coordinator that implements this contract is lead-session-orchestrator
12
+ (https://session-orchestrator.com/lead); any other tool that writes the same files works too.
11
13
 
12
14
  **Grundsatz (CSM-003).** Zustand liegt in Dateien; Nachrichten sind nur Hinweise auf eine Datei.
13
15
  Der Navigator erteilt keine Freigaben — maßgeblich ist der Owner-Auftrag der Session. Eine Datei,
@@ -103,6 +105,14 @@ Bedingungen, die der Navigator einer Session schreibt (z. B. Agent-Caps, volle P
103
105
  Offload-Regel, Merge- und Zeitregeln, Besitzkonflikte); Inhalt und Form gehören dem Navigator. Die
104
106
  Session **liest** die Datei; sie wartet auf keine Nachricht. Keine Auflage ist eine Freigabe.
105
107
 
108
+ **Pflichtfeld `repo`** (#1520, navigator#190): absoluter Pfad auf Toplevel oder git-common-dir des
109
+ Ziel-Repos. Der Dateiname ist die semantische Session-ID, und die kollidiert über Repos hinweg.
110
+ Gelesen wird über `node scripts/fleet-auflagen.mjs <session_id>` (Code: `readAuflagen()`); die
111
+ Datei gilt nur, wenn `repo` und das eigene Repo auf dasselbe `git rev-parse --path-format=absolute
112
+ --git-common-dir` auflösen, ein Worktree passt also zu seinem Haupt-Checkout. Fehlt das Feld, passt
113
+ es nicht oder ist die Datei unlesbar: fail-closed, WARN auf stderr, die Datei zählt als nicht
114
+ vorhanden, die Standard-Auflagen gelten.
115
+
106
116
  **Standard-Auflagen (konservative Stufe).** Sie gelten ab `fallback_due_at` (stdout der
107
117
  Check-in-CLI), wenn bis dahin keine Auflagen-Datei liegt. Die Frist rechnet `fallbackDueAt()`:
108
118
  `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.