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
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ /** Native, dependency-free MCP stdio transport. No shell, WSL, or jq required. */
3
+ import { execFileSync } from 'node:child_process';
4
+ import { readFileSync, statSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { createInterface } from 'node:readline';
7
+ import { isSessionConfigHeading } from './lib/config/section-extractor.mjs';
8
+ import { isMainModule } from './lib/is-main-module.mjs';
9
+
10
+ const SERVER_INFO = { name: 'session-orchestrator', version: '2.0.0' };
11
+ const TOOLS = [
12
+ {
13
+ name: 'session_config',
14
+ description: 'Reads Session Config section from the project instruction file (CLAUDE.md or AGENTS.md alias)',
15
+ inputSchema: { type: 'object', properties: {}, required: [] },
16
+ },
17
+ {
18
+ name: 'session_metrics',
19
+ description: 'Reads the last 5 REAL session metrics entries from .orchestrator/metrics/sessions.jsonl (abandoned phantom stubs excluded)',
20
+ inputSchema: { type: 'object', properties: {}, required: [] },
21
+ },
22
+ ];
23
+ const textContent = (text) => ({ content: [{ type: 'text', text }] });
24
+ const errorResponse = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
25
+
26
+ /** Resolve the caller's repository without passing native paths through a shell. */
27
+ export function resolveProjectRoot(cwd = process.cwd()) {
28
+ try {
29
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
30
+ cwd, encoding: 'utf8', windowsHide: true, timeout: 10_000,
31
+ stdio: ['ignore', 'pipe', 'ignore'],
32
+ }).trim();
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function isFile(path, nonempty = false) {
39
+ try {
40
+ const stat = statSync(path);
41
+ return stat.isFile() && (!nonempty || stat.size > 0);
42
+ } catch (error) {
43
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false;
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ /** Read the same raw config body as the legacy shell tool (including comments). */
49
+ export function readSessionConfig(root) {
50
+ const file = ['CLAUDE.md', 'AGENTS.md'].map((name) => join(root, name))
51
+ .find((path) => isFile(path, true));
52
+ if (!file) return `No CLAUDE.md or AGENTS.md found at ${root}`;
53
+ const body = [];
54
+ let inSection = false;
55
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
56
+ if (isSessionConfigHeading(line)) {
57
+ inSection = true;
58
+ } else if (line.startsWith('## ')) {
59
+ inSection = false;
60
+ } else if (inSection) {
61
+ body.push(line);
62
+ }
63
+ }
64
+ // Shell command substitution removes LF bytes, not spaces or CR bytes.
65
+ return body.join('\n').replace(/\n+$/, '') || `No '## Session Config' section found in ${file}`;
66
+ }
67
+
68
+ /** Read the last five real records, with whole-ledger stub count and token coverage. */
69
+ export function readSessionMetrics(root) {
70
+ const file = ['.orchestrator', '.claude'].map((dir) => join(root, dir, 'metrics', 'sessions.jsonl'))
71
+ .find((path) => isFile(path));
72
+ if (!file) return 'No metrics found (checked .orchestrator/metrics/ and .claude/metrics/)';
73
+ const entries = [];
74
+ let stubCount = 0;
75
+ for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
76
+ let record;
77
+ try { record = JSON.parse(line); } catch { continue; }
78
+ // Torn writes and non-record JSON must not hide subsequent valid sessions.
79
+ if (!record || typeof record !== 'object' || Array.isArray(record)) continue;
80
+ if (record.status === 'abandoned') { stubCount++; continue; }
81
+ entries.push(record);
82
+ if (entries.length > 5) entries.shift();
83
+ }
84
+ if (!entries.length) {
85
+ return stubCount ? `No real sessions (abandoned stubs excluded: ${stubCount})`
86
+ : 'No metrics found (file is empty)';
87
+ }
88
+ const record = entries.findLast((entry) => (entry.total_token_input !== null && entry.total_token_input !== undefined)
89
+ || (entry.total_token_output !== null && entry.total_token_output !== undefined));
90
+ const coverage = record?.subagents_with_tokens ?? 0;
91
+ const summary = record
92
+ ? `tokens: ${record.total_token_input ?? '?'} in / ${record.total_token_output ?? '?'} out`
93
+ + ` (coverage: ${coverage} subagents)`
94
+ + (coverage === 0 ? ' ⚠ partial — subagent token data missing; total is not a reliable cost estimate' : '')
95
+ + ` [session: ${record.session_id ?? 'unknown'}]`
96
+ : 'tokens: no token data in last 5 sessions (subagent telemetry not yet captured)';
97
+ return entries.map((entry) => JSON.stringify(entry)).join('\n')
98
+ + `\n\nabandoned stubs excluded: ${stubCount}\n\n--- token summary ---\n${summary}`;
99
+ }
100
+
101
+ /** Handle one JSON-RPC line. null means a notification or blank line, not a reply. */
102
+ export function handleLine(line, { cwd = process.cwd(), resolveRoot = resolveProjectRoot } = {}) {
103
+ if (!line.trim()) return null;
104
+ let request;
105
+ try { request = JSON.parse(line); } catch { return errorResponse(null, -32700, 'Parse error'); }
106
+ if (!request || typeof request !== 'object' || Array.isArray(request)) {
107
+ return errorResponse(null, -32600, 'Invalid Request');
108
+ }
109
+ const hasId = Object.hasOwn(request, 'id');
110
+ const validId = request.id === null || typeof request.id === 'string' || typeof request.id === 'number';
111
+ const id = hasId && validId ? request.id : null;
112
+ if (request.jsonrpc !== '2.0' || typeof request.method !== 'string' || (hasId && !validId)) {
113
+ return errorResponse(id, -32600, 'Invalid Request');
114
+ }
115
+ if (!hasId || request.method.startsWith('notifications/')) return null;
116
+ let result;
117
+ switch (request.method) {
118
+ case 'initialize':
119
+ result = { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: SERVER_INFO };
120
+ break;
121
+ case 'tools/list':
122
+ result = { tools: TOOLS };
123
+ break;
124
+ case 'tools/call': {
125
+ const name = request.params?.name;
126
+ if (typeof name !== 'string') return errorResponse(id, -32602, 'Invalid tool name');
127
+ if (!TOOLS.some((tool) => tool.name === name)) return errorResponse(id, -32602, `Unknown tool: ${name}`);
128
+ try {
129
+ const root = resolveRoot(cwd);
130
+ result = textContent(root
131
+ ? (name === 'session_config' ? readSessionConfig(root) : readSessionMetrics(root))
132
+ : 'Error: not inside a git repository');
133
+ } catch {
134
+ return errorResponse(id, -32603, 'Unable to read session data');
135
+ }
136
+ break;
137
+ }
138
+ default:
139
+ return errorResponse(id, -32601, `Method not found: ${request.method}`);
140
+ }
141
+ return { jsonrpc: '2.0', id, result };
142
+ }
143
+
144
+ /** Start newline-delimited JSON-RPC on the supplied streams. */
145
+ export async function serve({ input = process.stdin, output = process.stdout, cwd = process.cwd() } = {}) {
146
+ const lines = createInterface({ input, crlfDelay: Infinity });
147
+ for await (const line of lines) {
148
+ const response = handleLine(line, { cwd });
149
+ if (response) output.write(`${JSON.stringify(response)}\n`);
150
+ }
151
+ }
152
+
153
+ if (isMainModule(import.meta.url)) {
154
+ serve().catch(() => {
155
+ process.stderr.write('session-orchestrator: MCP stdio transport failed.\n');
156
+ process.exitCode = 2;
157
+ });
158
+ }
@@ -38,8 +38,9 @@
38
38
  * "already_canonical":N,"unmappable":N,"parse_errors":N,"backup":"<path>|null"}
39
39
  */
40
40
 
41
- import { readFileSync, writeFileSync, renameSync, copyFileSync, existsSync } from 'node:fs';
42
- import { join, dirname, basename } from 'node:path';
41
+ import { readFileSync, copyFileSync, existsSync } from 'node:fs';
42
+ import { basename } from 'node:path';
43
+ import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
43
44
  import { validateSession, normalizeSession, ValidationError } from './lib/session-schema.mjs';
44
45
  import { isMainModule } from './lib/is-main-module.mjs';
45
46
 
@@ -422,11 +423,12 @@ async function main() {
422
423
  // Atomic write: backup → tmp → rename
423
424
  const iso = new Date().toISOString().replace(/[:.]/g, '-');
424
425
  const backupPath = `${args.file}.bak-${iso}`;
425
- const tmpPath = join(dirname(args.file), `.${basename(args.file)}.migrate-tmp-${process.pid}`);
426
426
  try {
427
427
  copyFileSync(args.file, backupPath);
428
- writeFileSync(tmpPath, outputLines.join('\n') + '\n', 'utf8');
429
- renameSync(tmpPath, args.file);
428
+ const res = atomicWriteWithBackup(args.file, outputLines.join('\n') + '\n', {
429
+ tmpPrefix: `.${basename(args.file)}.tmp`,
430
+ });
431
+ if (!res.ok) throw envelopeToError(res);
430
432
  summary.backup = backupPath;
431
433
  } catch (err) {
432
434
  const hint = existsSync(backupPath)
@@ -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
@@ -387,11 +387,14 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
387
387
  // `scripts/lib/locks/index.mjs` is the one-cycle deprecation shim (5.2.0 CHANGELOG, Changed): its
388
388
  // runtime warning names the version it was deprecated IN, so it legitimately carries the previous
389
389
  // release literal until its 6.0.0 removal — version history by construction, not drift.
390
+ // `skills/eval/rubric-v3.md` (5.9.0 sweep, measured 2026-10-06): its change log quotes the dated
391
+ // owner decision "rubric-v3 in 5.8.0" (2026-10-05, #1487) — the release that introduced the rubric.
392
+ // Prose history, not a version surface; bumping it would falsify the quote.
390
393
  // `site/_census.json` is deliberately NOT allowlisted: it carries a `"version"` field that
391
394
  // `--set-version` re-stamps (scripts/site-numbers.mjs --write, see the post-bump hint in main()),
392
395
  // so leaving it under the drift sweep makes the sweep double as its staleness gate — a census
393
396
  // not re-stamped after a bump fails `--check` instead of shipping the previous version.
394
- export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|scripts\/lib\/locks\/index\.mjs|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|skills\/release\/SKILL\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
397
+ export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|scripts\/lib\/locks\/index\.mjs|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|skills\/eval\/rubric-v3\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|skills\/release\/SKILL\.md|marketing\/remotion\/(?:README\.md|campaign\.json|render-receipt\.json|src\/ReleaseFilm\.tsx)$)/;
395
398
 
396
399
  /** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
397
400
  export function checkLeakage(lines) {
@@ -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,
@@ -55,8 +55,8 @@ import {
55
55
  resolveGateTimeoutMs,
56
56
  } from './lib/gates/gate-helpers.mjs';
57
57
  import { buildCommandSignature, spawnInGroup } from './lib/process-group.mjs';
58
- import { classifyManifestSession, readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
59
- import { findOwnScopeFile } from './lib/scope-gate.mjs';
58
+ import { resolveWaveNumber } from './lib/quality-gate-wave.mjs';
59
+ import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
60
60
  import { gateKillFields } from './lib/quality-gate.mjs';
61
61
  import { isMainModule } from './lib/is-main-module.mjs';
62
62
  import { shellQuote } from './lib/sh-quote.mjs';
@@ -266,42 +266,6 @@ function resolveLedgerRoot(value) {
266
266
  return null;
267
267
  }
268
268
 
269
- /**
270
- * Resolve the active wave number from the wave-scope sidecar (#966 step 1).
271
- *
272
- * Mirrors `resolveWave()` in `hooks/pre-bash-memory-propose-audit.mjs` — the
273
- * same `.{pi,cursor,codex,claude}/wave-scope.json` precedence via
274
- * {@link findOwnScopeFile}, skipping a PEER session's manifest in the same
275
- * working copy (#1504: a peer-only result is no wave of ours, never the peer's
276
- * number) — with ONE deliberate difference: the hook returns `0` for "no
277
- * wave-scope file", this returns `null`.
278
- *
279
- * Absent is not zero. A human running `npm run quality-gate` from a `git push`
280
- * has no wave at all, and that is the common case; publishing `wave_number: 0`
281
- * would invent a wave 0 that every consumer then has to special-case. The
282
- * caller spreads the result so the KEY is omitted, exactly as `counts` is.
283
- *
284
- * A non-positive or non-numeric `wave` field is treated the same way — waves
285
- * are 1-indexed, so `0` on disk carries no more information than an absent file.
286
- *
287
- * Never throws.
288
- *
289
- * @param {string} projectDir — directory whose wave-scope sidecar to read.
290
- * @returns {number|null} positive wave number, or `null` when there is no wave.
291
- */
292
- function resolveWaveNumber(projectDir) {
293
- try {
294
- const ownIds = new Set(readProcessLocalSessionIds({ env: process.env, hookInput: null }));
295
- const { path: waveFile } = findOwnScopeFile(projectDir, ownIds, classifyManifestSession);
296
- if (!waveFile || !existsSync(waveFile)) return null;
297
- const wave = JSON.parse(readFileSync(waveFile, 'utf8'))?.wave;
298
- if (typeof wave !== 'number' || !Number.isFinite(wave) || wave <= 0) return null;
299
- return Math.trunc(wave);
300
- } catch {
301
- return null;
302
- }
303
- }
304
-
305
269
  // ---------------------------------------------------------------------------
306
270
  // Main — the CLI body. Guarded below so a bare `import()` runs none of it.
307
271
  // ---------------------------------------------------------------------------
@@ -471,7 +435,13 @@ async function main() {
471
435
  //
472
436
  // stdout is PIPED (not inherited) so the suite counts the gate already computed
473
437
  // can be lifted straight off its JSON envelope into telemetry (#954) instead of
474
- // travelling as prose through the STATE.md header. The envelope is re-emitted
438
+ // travelling as prose through the STATE.md header. Both skill readers select
439
+ // the last matching flat event by semantic_session_id + wave_number; there is
440
+ // no platform field on the payload. Count fallback (wave-loop step 7: this
441
+ // wave's own gate output; session end: the STATE.md header) is limited to
442
+ // pre-#954 sessions, gates outside the event wrapper, or missing wave
443
+ // attribution (see docs/events-schema.md).
444
+ // The envelope is re-emitted
475
445
  // verbatim below, so the stdout contract is unchanged — a gate sub-script writes
476
446
  // exactly one JSON line at the very end (its own child commands are captured by
477
447
  // `runCheck`), so nothing streamed before and nothing streams now.