session-orchestrator 5.7.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 (190) 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 +49 -0
  42. package/README.md +10 -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 +4 -3
  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/config/drift-check.mjs +1 -1
  94. package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
  95. package/scripts/lib/config.mjs +5 -1
  96. package/scripts/lib/convergence-monitor.mjs +5 -3
  97. package/scripts/lib/ecosystem-health.mjs +5 -3
  98. package/scripts/lib/eval/engine.mjs +61 -27
  99. package/scripts/lib/eval/judge.mjs +14 -8
  100. package/scripts/lib/eval/session-resolve.mjs +7 -2
  101. package/scripts/lib/events-retention-banner.mjs +5 -2
  102. package/scripts/lib/events.mjs +19 -5
  103. package/scripts/lib/fleet-protocol.mjs +98 -1
  104. package/scripts/lib/instruction-budget-guard.mjs +25 -18
  105. package/scripts/lib/io.mjs +172 -35
  106. package/scripts/lib/learnings/candidates.mjs +7 -28
  107. package/scripts/lib/learnings/io.mjs +7 -2
  108. package/scripts/lib/learnings/judgment.mjs +5 -8
  109. package/scripts/lib/learnings/select.mjs +3 -3
  110. package/scripts/lib/maintenance-due-banner.mjs +2 -2
  111. package/scripts/lib/memory-proposals/sink.mjs +5 -6
  112. package/scripts/lib/memory-proposals/store.mjs +3 -5
  113. package/scripts/lib/mirror-issues-banner.mjs +11 -1
  114. package/scripts/lib/native-command.mjs +48 -0
  115. package/scripts/lib/orphan-reaper/scan.mjs +29 -3
  116. package/scripts/lib/peer-cards/writer.mjs +6 -18
  117. package/scripts/lib/platform.mjs +113 -4
  118. package/scripts/lib/plugin-package-stage.mjs +169 -0
  119. package/scripts/lib/plugin-surface.mjs +121 -0
  120. package/scripts/lib/quality-gate.mjs +30 -25
  121. package/scripts/lib/reconcile/idempotency.mjs +9 -10
  122. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +48 -11
  123. package/scripts/lib/reconcile/writer.mjs +11 -17
  124. package/scripts/lib/rule-loader.mjs +32 -6
  125. package/scripts/lib/scope-echo.mjs +12 -2
  126. package/scripts/lib/scope-gate.mjs +2 -1
  127. package/scripts/lib/session-close-backfill.mjs +3 -4
  128. package/scripts/lib/session-id.mjs +2 -1
  129. package/scripts/lib/session-record-repair.mjs +9 -13
  130. package/scripts/lib/session-schema/constants.mjs +20 -5
  131. package/scripts/lib/session-start-probes.mjs +2 -2
  132. package/scripts/lib/sessions-staleness-banner.mjs +2 -3
  133. package/scripts/lib/skill-evolution/engine.mjs +6 -9
  134. package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
  135. package/scripts/lib/slopcheck.mjs +9 -7
  136. package/scripts/lib/sunset/walker.mjs +3 -3
  137. package/scripts/lib/telemetry/sync.mjs +3 -1
  138. package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
  139. package/scripts/lib/test-sibling-coverage.mjs +167 -0
  140. package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
  141. package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
  142. package/scripts/lib/user-invocable-skills.mjs +37 -10
  143. package/scripts/lib/ux-grill/run-record.mjs +5 -3
  144. package/scripts/lib/validate/check-banner-parity.mjs +1 -0
  145. package/scripts/lib/validate/check-skills.mjs +12 -2
  146. package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
  147. package/scripts/lib/validate/check-test-value-bans.mjs +3 -2
  148. package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
  149. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  150. package/scripts/lib/vault-archive.mjs +2 -4
  151. package/scripts/lib/vault-mirror/process.mjs +207 -26
  152. package/scripts/lib/vault-mirror/utils.mjs +16 -1
  153. package/scripts/lib/vault-sync-baseline.mjs +9 -8
  154. package/scripts/lib/wave-transcript-tail.mjs +8 -4
  155. package/scripts/lib/worktree/meta.mjs +7 -3
  156. package/scripts/mcp-server.mjs +158 -0
  157. package/scripts/migrate-sessions-jsonl.mjs +7 -5
  158. package/scripts/migrate-vault-paths.mjs +7 -4
  159. package/scripts/print-learnings-index.mjs +2 -1
  160. package/scripts/relocate-vault-corpus.mjs +5 -3
  161. package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
  162. package/scripts/self-update.mjs +179 -39
  163. package/scripts/site-numbers.mjs +4 -3
  164. package/scripts/sweep-expired-learnings.mjs +6 -4
  165. package/scripts/sync-vault-schema.mjs +3 -7
  166. package/scripts/windows-setup.mjs +45 -0
  167. package/skills/_shared/fleet-protocol.md +8 -0
  168. package/skills/architecture/SKILL.md +1 -0
  169. package/skills/bootstrap/_shared-template.md +5 -2
  170. package/skills/claude-md-drift-check/SKILL.md +4 -1
  171. package/skills/claude-md-drift-check/checker.mjs +83 -10
  172. package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
  173. package/skills/eval/SKILL.md +12 -13
  174. package/skills/eval/rubric-v3.md +521 -0
  175. package/skills/frontmatter-guard/SKILL.md +1 -0
  176. package/skills/gitlab-portfolio/SKILL.md +1 -0
  177. package/skills/hook-development/SKILL.md +1 -0
  178. package/skills/mcp-builder/SKILL.md +1 -0
  179. package/skills/repo-audit/SKILL.md +2 -2
  180. package/skills/session-end/SKILL.md +32 -1
  181. package/skills/session-end/vault-operations.md +12 -2
  182. package/skills/session-start/SKILL.md +2 -2
  183. package/skills/session-start/phase-7-1-premise-check.md +2 -1
  184. package/skills/session-start/references/operations-contract.md +4 -1
  185. package/skills/tmux-layout/SKILL.md +1 -0
  186. package/skills/vault-mirror/SKILL.md +1 -0
  187. package/skills/vault-sync/SKILL.md +6 -6
  188. package/skills/vault-sync/validator.sh +29 -10
  189. package/skills/wave-executor/references/wave-loop-scope-manifest.md +4 -4
  190. package/skills/write-executable-plan/SKILL.md +1 -0
@@ -58,6 +58,7 @@ import {
58
58
  } from './lib/vault-migration-rules.mjs';
59
59
  import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
60
60
  import { isMainModule } from './lib/is-main-module.mjs';
61
+ import { atomicWriteText, envelopeToError } from './lib/io.mjs';
61
62
 
62
63
  // ---------------------------------------------------------------------------
63
64
  // Constants
@@ -529,12 +530,14 @@ function findHits(filePath, content) {
529
530
  }
530
531
 
531
532
  /**
532
- * Atomic write: write to <path>.migrate-tmp, then rename.
533
+ * Atomic write via `atomicWriteText` (`<dir>/.<name>.tmp.<hex>`, then rename);
534
+ * throws with the original message and `code` on failure.
533
535
  */
534
536
  async function atomicWrite(filePath, content) {
535
- const tmp = `${filePath}.migrate-tmp-${process.pid}-${Date.now()}`;
536
- await fs.writeFile(tmp, content, 'utf8');
537
- await fs.rename(tmp, filePath);
537
+ const res = await atomicWriteText(filePath, content, {
538
+ tmpPrefix: `.${path.basename(filePath)}.tmp`,
539
+ });
540
+ if (!res.ok) throw envelopeToError(res);
538
541
  }
539
542
 
540
543
  /**
@@ -101,6 +101,7 @@ import { join, dirname } from 'node:path';
101
101
  import { fileURLToPath } from 'node:url';
102
102
 
103
103
  import { findProjectRoot } from './lib/common.mjs';
104
+ import { eventsFilePath } from './lib/events.mjs';
104
105
  import { resolveStateArtifactPath } from './lib/state-md.mjs';
105
106
  import {
106
107
  CANDIDATE_POOL_SIZE,
@@ -249,7 +250,7 @@ const repoRoot = findProjectRoot(process.cwd());
249
250
  const learningsPath = opts.learnings
250
251
  ? opts.learnings
251
252
  : join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
252
- const eventsPath = join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
253
+ const eventsPath = eventsFilePath(repoRoot); // #1514 point 1: the repo's one ledger
253
254
  // #1019 — the natively-delivered rule corpus. Every `.claude/rules/*.md` reaches
254
255
  // a dispatched agent in FULL through Claude Code's own project-instruction
255
256
  // loading (`docs/instruction-delivery.md` §1: the `globs:`/`tier:` frontmatter is
@@ -55,6 +55,7 @@ import {
55
55
  import { buildBackfillIndex, parseSessionId } from './lib/vault-repo-backfill.mjs';
56
56
  import { parseColumnFlags, CliFlagError } from './lib/cli-flags.mjs';
57
57
  import { isMainModule } from './lib/is-main-module.mjs';
58
+ import { atomicWriteText, envelopeToError } from './lib/io.mjs';
58
59
 
59
60
  // ---------------------------------------------------------------------------
60
61
  // Constants
@@ -693,7 +694,6 @@ async function writeManifest({ vaultDir, derivableOnly, summary, moves }) {
693
694
 
694
695
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
695
696
  const manifestPath = path.join(manifestDir, `relocation-manifest-${ts}.json`);
696
- const tmpPath = `${manifestPath}.tmp-${process.pid}`;
697
697
 
698
698
  // Store vault-relative paths in the manifest
699
699
  const movesRel = moves.map((m) => ({
@@ -714,8 +714,10 @@ async function writeManifest({ vaultDir, derivableOnly, summary, moves }) {
714
714
  moves: movesRel,
715
715
  };
716
716
 
717
- await fs.writeFile(tmpPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
718
- await fs.rename(tmpPath, manifestPath);
717
+ const res = await atomicWriteText(manifestPath, JSON.stringify(manifest, null, 2) + '\n', {
718
+ tmpPrefix: `.${path.basename(manifestPath)}.tmp`,
719
+ });
720
+ if (!res.ok) throw envelopeToError(res);
719
721
 
720
722
  return manifestPath;
721
723
  }
@@ -32,9 +32,9 @@
32
32
  * with the next repo, and exits 2 at the end
33
33
  */
34
34
 
35
- import { existsSync, readFileSync, writeFileSync, copyFileSync, renameSync } from 'node:fs';
35
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
36
36
  import { mkdirSync } from 'node:fs';
37
- import { dirname, join } from 'node:path';
37
+ import { basename, dirname, join } from 'node:path';
38
38
  import { homedir } from 'node:os';
39
39
  import {
40
40
  migrateLegacyLearning,
@@ -43,6 +43,7 @@ import {
43
43
  import { LearningsLockError, withLearningsLock } from './lib/learnings/io.mjs';
44
44
  import { getCrossRepoProjects, getConfinementRoot } from './lib/config/cross-repo.mjs';
45
45
  import { validatePathInsideProject } from './lib/path-utils.mjs';
46
+ import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
46
47
 
47
48
  const LEARNINGS_REL = '.orchestrator/metrics/learnings.jsonl';
48
49
 
@@ -316,12 +317,13 @@ function migrateStore(repoPath, learningsPath, apply) {
316
317
  try {
317
318
  // Backup original
318
319
  copyFileSync(learningsPath, backupPath);
319
- // Write migrated content atomically
320
+ // Write migrated content atomically (own backup above keeps its
321
+ // `.bak-cross-repo-migrate-<ms>` schema, so the helper takes none)
320
322
  const body = outputLines.join('\n') + '\n';
321
- const tmpPath = `${learningsPath}.migrate-cross-repo-tmp-${process.pid}-${timestamp}`;
322
- mkdirSync(dirname(learningsPath), { recursive: true });
323
- writeFileSync(tmpPath, body, 'utf8');
324
- renameSync(tmpPath, learningsPath);
323
+ const res = atomicWriteWithBackup(learningsPath, body, {
324
+ tmpPrefix: `.${basename(learningsPath)}.tmp`,
325
+ });
326
+ if (!res.ok) throw envelopeToError(res);
325
327
  } catch (err) {
326
328
  return {
327
329
  repo: repoPath,
@@ -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
  {
@@ -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
+ }
@@ -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.