ruvnet-brain 4.3.3 → 4.3.8

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.
@@ -25,7 +25,8 @@
25
25
  import fs from 'node:fs';
26
26
  import os from 'node:os';
27
27
  import path from 'node:path';
28
- import { spawnSync } from 'node:child_process';
28
+ import { spawn, spawnSync } from 'node:child_process';
29
+ import { pathToFileURL } from 'node:url';
29
30
 
30
31
  /** The three host shapes a release must survive. ONE name each, for every consumer. */
31
32
  export const HOST_MODES = Object.freeze(['claude', 'codex', 'dual']);
@@ -58,7 +59,7 @@ export const MODE_FROM_RECEIPT_NAME = Object.freeze({ claudeOnly: 'claude', code
58
59
  export const VARIANTS = Object.freeze({
59
60
  staged: Object.freeze({
60
61
  installerArgs: (_v) => ['--local', '--yes', '--force', '--no-nightly-prompt',
61
- '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline', '--no-selfcheck'],
62
+ '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline', '--no-selfcheck', '--no-verify'],
62
63
  env: ({ packageRoot }) => ({
63
64
  RUVNET_CLAUDE_MARKETPLACE_SOURCE: packageRoot,
64
65
  RUVNET_CODEX_HOOK_TRUST_MODE: 'bypass',
@@ -131,46 +132,252 @@ export function runHostMatrix({ packageRoot, version, variant = 'staged', locate
131
132
  let error;
132
133
 
133
134
  for (const mode of HOST_MODES) {
134
- try {
135
- const home = path.join(workspace, `home-${mode}`);
136
- const codexHome = path.join(home, '.codex');
137
- const brainHome = path.join(home, '.cache', 'ruvnet-brain');
138
- fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
139
- if (mode !== 'claude') fs.mkdirSync(codexHome, { recursive: true });
140
- const env = {
141
- ...process.env,
142
- HOME: home,
143
- CODEX_HOME: codexHome,
144
- RUVNET_BRAIN_HOME: brainHome,
145
- RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
146
- CI: 'true',
147
- PATH: fixturePath(mode, workspace, locate),
148
- ...spec.env({ packageRoot }),
149
- };
150
- const install = run(process.execPath, [installer, ...spec.installerArgs(version)], {
151
- cwd: packageRoot, env, encoding: 'utf8', timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024,
152
- });
153
- if (install.error || install.status !== 0) {
154
- throw new Error(`install failed for ${mode}: ${(install.stderr || install.error?.message || '').slice(-4000)}`);
155
- }
156
- const doctor = run(process.execPath, [installer, '--doctor', '--hooks'], {
157
- cwd: packageRoot, env, encoding: 'utf8', timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
158
- });
159
- const classified = classifyDoctor(doctor);
160
- fixtures[mode] = { status: classified.status, doctorExit: doctor.status, version };
161
- if (!classified.accepted) {
162
- verdict = 'FAIL';
163
- fixtures[mode].output = classified.output.slice(-5000);
164
- // The output belongs IN the error. The previous harness put it there and I dropped it in
165
- // the consolidation, so CI reported a bare "doctor failed for claude" and the one thing
166
- // needed to act on it — what the doctor actually said — was thrown away.
167
- error = error || `doctor failed for ${mode} (exit ${doctor.status}): ${classified.output.slice(-4000)}`;
168
- }
169
- } catch (e) {
135
+ const result = runHostMode({ mode, packageRoot, version, spec, workspace, locate, run });
136
+ if (result.error) {
170
137
  verdict = 'FAIL';
171
- fixtures[mode] = { status: 'FAIL', error: e.message };
172
- error = error || e.message;
138
+ fixtures[mode] = result.fixture;
139
+ error = error || result.error;
140
+ } else {
141
+ fixtures[mode] = result.fixture;
173
142
  }
174
143
  }
175
144
  return error ? { verdict, fixtures, error } : { verdict, fixtures };
176
145
  }
146
+
147
+ function runHostMode({ mode, packageRoot, version, spec, workspace, locate, run }) {
148
+ try {
149
+ const home = path.join(workspace, `home-${mode}`);
150
+ const codexHome = path.join(home, '.codex');
151
+ const brainHome = path.join(home, '.cache', 'ruvnet-brain');
152
+ fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
153
+ if (mode !== 'claude') fs.mkdirSync(codexHome, { recursive: true });
154
+ const env = {
155
+ ...process.env,
156
+ HOME: home,
157
+ CODEX_HOME: codexHome,
158
+ RUVNET_BRAIN_HOME: brainHome,
159
+ RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
160
+ CI: 'true',
161
+ PATH: fixturePath(mode, workspace, locate),
162
+ ...spec.env({ packageRoot }),
163
+ };
164
+ const installer = path.join(packageRoot, 'bin', 'install.mjs');
165
+ const install = run(process.execPath, [installer, ...spec.installerArgs(version)], {
166
+ cwd: packageRoot, env, encoding: 'utf8', timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024,
167
+ });
168
+ if (install.error || install.status !== 0) {
169
+ throw new Error(`install failed for ${mode}: ${(install.stderr || install.error?.message || '').slice(-4000)}`);
170
+ }
171
+ const doctor = run(process.execPath, [installer, '--doctor', '--hooks'], {
172
+ cwd: packageRoot, env, encoding: 'utf8', timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
173
+ });
174
+ const classified = classifyDoctor(doctor);
175
+ const fixture = { status: classified.status, doctorExit: doctor.status, version };
176
+ if (!classified.accepted) {
177
+ fixture.output = classified.output.slice(-5000);
178
+ return {
179
+ fixture,
180
+ error: `doctor failed for ${mode} (exit ${doctor.status}): ${classified.output.slice(-4000)}`,
181
+ };
182
+ }
183
+ return { fixture };
184
+ } catch (e) {
185
+ return { fixture: { status: 'FAIL', error: e.message }, error: e.message };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Async equivalent used by hosted release qualification. Each mode owns its HOME and PATH, so
191
+ * the expensive installer/doctor pairs can run concurrently without sharing mutable state.
192
+ * Results are reassembled in HOST_MODES order before the single caller writes its receipt.
193
+ */
194
+ export async function runHostMatrixAsync({
195
+ packageRoot,
196
+ version,
197
+ variant = 'staged',
198
+ locate,
199
+ temp,
200
+ runCommand = spawnCommand,
201
+ runMcpSearch = runInstalledMcpSearch,
202
+ verifyGrounding = verifyInstalledGrounding,
203
+ resolveMcpServer = resolveInstalledMcpServer,
204
+ }) {
205
+ const spec = VARIANTS[variant];
206
+ if (!spec) throw new Error(`unknown host-matrix variant: ${variant}`);
207
+ const workspace = temp || fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-host-matrix-'));
208
+ const sharedModelCache = path.join(workspace, 'models-cache');
209
+ fs.mkdirSync(sharedModelCache, { recursive: true });
210
+ const contexts = HOST_MODES.map((mode) => {
211
+ const home = path.join(workspace, `home-${mode}`);
212
+ const codexHome = path.join(home, '.codex');
213
+ const brainHome = path.join(home, '.cache', 'ruvnet-brain');
214
+ fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
215
+ if (mode !== 'claude') fs.mkdirSync(codexHome, { recursive: true });
216
+ const env = {
217
+ ...process.env,
218
+ HOME: home,
219
+ CODEX_HOME: codexHome,
220
+ RUVNET_BRAIN_HOME: brainHome,
221
+ RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
222
+ KB_MODEL_CACHE: sharedModelCache,
223
+ CI: 'true',
224
+ PATH: fixturePath(mode, workspace, locate),
225
+ ...spec.env({ packageRoot }),
226
+ };
227
+ return { mode, home, env };
228
+ });
229
+ const installer = path.join(packageRoot, 'bin', 'install.mjs');
230
+ const installs = await Promise.all(contexts.map(async (context) => {
231
+ const processResult = await runCommand(process.execPath, [installer, ...spec.installerArgs(version)], {
232
+ cwd: packageRoot, env: context.env, timeout: 1_200_000,
233
+ });
234
+ return { context, processResult };
235
+ }));
236
+ const failedInstall = installs.find(({ processResult }) => processResult.error || processResult.status !== 0);
237
+ if (failedInstall) {
238
+ const detail = processDiagnostic(failedInstall.processResult);
239
+ return {
240
+ verdict: 'FAIL',
241
+ fixtures: Object.fromEntries(contexts.map(({ mode }) => [mode, {
242
+ status: mode === failedInstall.context.mode ? 'FAIL' : 'NOT_PROBED',
243
+ ...(mode === failedInstall.context.mode ? { process: processIdentity(failedInstall.processResult) } : {}),
244
+ }])),
245
+ error: `install failed for ${failedInstall.context.mode} (${detail})`,
246
+ };
247
+ }
248
+
249
+ const prewarmContext = contexts[0];
250
+ const prewarmReader = path.join(prewarmContext.env.RUVNET_BRAIN_KB, 'forge-ask-all.mjs');
251
+ const prewarm = await runCommand(process.execPath, [prewarmReader, '--dir', prewarmContext.env.RUVNET_BRAIN_KB,
252
+ '--q', 'How does RuvNet Brain prove a public release artifact?', '--k', '1'], {
253
+ cwd: prewarmContext.env.RUVNET_BRAIN_KB, env: prewarmContext.env, timeout: 300_000,
254
+ });
255
+ if (prewarm.error || prewarm.status !== 0) {
256
+ return { verdict: 'FAIL', fixtures: {}, error: `shared model prewarm failed (${processDiagnostic(prewarm)})` };
257
+ }
258
+ const prewarmGrounding = await verifyGrounding(String(prewarm.stdout || ''), prewarmContext.env.RUVNET_BRAIN_KB);
259
+ if (!prewarmGrounding?.grounded) {
260
+ return { verdict: 'FAIL', fixtures: {}, error: 'shared model prewarm returned no grounded source receipt' };
261
+ }
262
+
263
+ const searches = await Promise.all(contexts.map(async (context) => {
264
+ const serverPath = resolveMcpServer(context);
265
+ const processResult = await runMcpSearch({ mode: context.mode, serverPath, env: context.env });
266
+ const output = `${processResult.stdout || ''}${processResult.stderr || ''}`;
267
+ if (processResult.error || processResult.status !== 0) {
268
+ return { context, processResult, error: `MCP search failed for ${context.mode} (${processDiagnostic(processResult)}): ${output.slice(-2000)}` };
269
+ }
270
+ const grounding = await verifyGrounding(output, context.env.RUVNET_BRAIN_KB);
271
+ if (!grounding?.grounded) return { context, processResult, error: `MCP search grounding unproven for ${context.mode}` };
272
+ return { context, processResult, grounding };
273
+ }));
274
+ const fixtures = Object.fromEntries(searches.map(({ context, processResult, grounding, error }) => [context.mode, {
275
+ status: error ? 'FAIL' : 'PASS',
276
+ version,
277
+ process: processIdentity(processResult),
278
+ ...(grounding?.receipt ? { grounding: grounding.receipt } : {}),
279
+ ...(error ? { error } : {}),
280
+ }]));
281
+ const error = searches.find((result) => result.error)?.error;
282
+ return error ? { verdict: 'FAIL', fixtures, error } : { verdict: 'PASS', fixtures };
283
+ }
284
+
285
+ export function resolveInstalledMcpServer({ mode, home }) {
286
+ if (mode !== 'claude') return path.join(home, '.claude', 'ruvnet-brain', 'mcp', 'server.mjs');
287
+ const registry = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
288
+ const rows = JSON.parse(fs.readFileSync(registry, 'utf8'))?.plugins?.['ruvnet-brain@ruvnet-brain'];
289
+ const installPath = Array.isArray(rows) ? rows.find(({ scope }) => scope === 'user')?.installPath : null;
290
+ if (!installPath) throw new Error('Claude fixture has no user-scoped ruvnet-brain plugin install');
291
+ const managedRoot = path.resolve(home, '.claude', 'plugins', 'cache', 'ruvnet-brain', 'ruvnet-brain');
292
+ const resolved = path.resolve(installPath);
293
+ if (resolved !== managedRoot && !resolved.startsWith(`${managedRoot}${path.sep}`)) {
294
+ throw new Error('Claude fixture plugin install path escapes its managed cache');
295
+ }
296
+ const server = path.join(resolved, 'mcp', 'server.mjs');
297
+ if (!fs.existsSync(server)) throw new Error(`Claude fixture MCP server missing from installed plugin: ${server}`);
298
+ return server;
299
+ }
300
+
301
+ function processIdentity(result) {
302
+ return {
303
+ status: result.status ?? null,
304
+ signal: result.signal ?? null,
305
+ errorCode: result.error?.code ?? null,
306
+ errorMessage: result.error?.message ?? null,
307
+ };
308
+ }
309
+
310
+ function processDiagnostic(result) {
311
+ const process = processIdentity(result);
312
+ return `status=${process.status} signal=${process.signal || 'none'} error=${process.errorCode ? `${process.errorCode}: ${process.errorMessage}` : 'none'}`;
313
+ }
314
+
315
+ async function verifyInstalledGrounding(output, kbDir) {
316
+ const verifier = path.join(kbDir, 'verify-citation.mjs');
317
+ const { verifyGrounding } = await import(pathToFileURL(verifier).href);
318
+ return verifyGrounding(output, kbDir);
319
+ }
320
+
321
+ function runInstalledMcpSearch({ serverPath, env, query = 'How does RuvNet Brain prove a public release artifact?', timeout = 300_000 }) {
322
+ return new Promise((resolve) => {
323
+ const child = spawn(process.execPath, [serverPath], { env, stdio: ['pipe', 'pipe', 'pipe'] });
324
+ let stdout = '';
325
+ let stderr = '';
326
+ const pending = new Map();
327
+ let settled = false;
328
+ const finish = (result) => {
329
+ if (settled) return;
330
+ settled = true;
331
+ clearTimeout(timer);
332
+ child.kill('SIGTERM');
333
+ resolve({ status: result.status ?? null, signal: result.signal ?? null, error: result.error ?? null, stdout, stderr });
334
+ };
335
+ const timer = setTimeout(() => finish({ status: null, signal: 'SIGKILL', error: Object.assign(new Error('MCP search timed out'), { code: 'ETIMEDOUT' }) }), timeout);
336
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
337
+ child.stdout.on('data', (chunk) => {
338
+ stdout += chunk;
339
+ for (;;) {
340
+ const newline = stdout.indexOf('\n');
341
+ if (newline < 0) break;
342
+ const line = stdout.slice(0, newline).trim();
343
+ stdout = stdout.slice(newline + 1);
344
+ if (!line) continue;
345
+ let message;
346
+ try { message = JSON.parse(line); } catch { continue; }
347
+ const handler = pending.get(message.id);
348
+ if (handler) { pending.delete(message.id); handler(message); }
349
+ }
350
+ });
351
+ child.on('error', (error) => finish({ status: null, error }));
352
+ child.on('exit', (status, signal) => {
353
+ if (!settled) finish({ status, signal, ...(status === 0 ? {} : { error: new Error(`MCP server exited ${status}`) }) });
354
+ });
355
+ const call = (id, method, params = {}) => new Promise((done) => {
356
+ pending.set(id, done);
357
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
358
+ });
359
+ (async () => {
360
+ const initialized = await call(1, 'initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'release-host-matrix', version: '1' } });
361
+ if (initialized.error) throw new Error(`MCP initialize failed: ${JSON.stringify(initialized.error)}`);
362
+ const listed = await call(2, 'tools/list');
363
+ if (!listed.result?.tools?.some((tool) => tool.name === 'search_ruvnet')) throw new Error('installed MCP does not advertise search_ruvnet');
364
+ const searched = await call(3, 'tools/call', { name: 'search_ruvnet', arguments: { query, k: 5 } });
365
+ const text = (searched.result?.content || []).map((item) => item.text || '').join('\n');
366
+ if (searched.error || searched.result?.isError) throw new Error(`installed Brain search failed: ${text.slice(0, 400)}`);
367
+ stdout = text;
368
+ finish({ status: 0 });
369
+ })().catch((error) => finish({ status: null, error }));
370
+ });
371
+ }
372
+
373
+ function spawnCommand(command, args, options) {
374
+ return new Promise((resolve) => {
375
+ const child = spawn(command, args, options);
376
+ const stdout = [];
377
+ const stderr = [];
378
+ child.stdout?.on('data', (chunk) => stdout.push(chunk));
379
+ child.stderr?.on('data', (chunk) => stderr.push(chunk));
380
+ child.on('close', (status, signal) => resolve({ status, signal, stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString() }));
381
+ child.on('error', (error) => resolve({ status: null, error, stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString() }));
382
+ });
383
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ export const EXCLUSION_POLICY = 'release-linux-v1';
8
+
9
+ const ALLOWED_LINUX_SKIP_TITLES = new Set([
10
+ 'stores, exact-retrieves, and strictly deduplicates through a temp real AgentDB',
11
+ 'exposes self-contained native Console and What is New skills through the real plugin loader',
12
+ 'repairs a missing Brain-owned marketplace snapshot before Codex reads plugin state',
13
+ 'repairs a malformed Brain-owned marketplace snapshot before Codex reads plugin state',
14
+ 'repairs the snapshot without re-enabling an explicitly disabled Codex plugin',
15
+ 'reports a marketplace preparation failure without changing Codex configuration',
16
+ 'bridges, ranks, and is delivered as additionalContext at the write-code decision point',
17
+ 'TEETH: an UNTAGGED row travels no further than the store — the chain is genuinely load-bearing',
18
+ 'TEETH: a lesson tagged for a DIFFERENT moment does not fire at this one',
19
+ 'TEETH: a value too long to read from search output resolves in FULL via retrieve',
20
+ 'a USER-LEVEL store at an explicit --path is reachable the same way',
21
+ 'TEETH: a MISSING key reports not-found rather than inventing content',
22
+ '`-y` alone does NOT install the nightly LaunchAgent',
23
+ '`--yes` alone does NOT install the spend-watchdog LaunchAgent',
24
+ 'embeds only the changed file on pass two and preserves the unchanged stable ID',
25
+ ]);
26
+
27
+ const digestNames = (names) => crypto.createHash('sha256')
28
+ .update(`${JSON.stringify([...names].sort())}\n`)
29
+ .digest('hex');
30
+
31
+ export function buildIntegrationEvidence(report, { sourceSha, runId, runAttempt }) {
32
+ const total = Number(report.numTotalTests || 0);
33
+ const failed = Number(report.numFailedTests || 0) + Number(report.numFailedTestSuites || 0);
34
+ const skipped = Number(report.numPendingTests || 0);
35
+ const todo = Number(report.numTodoTests || 0);
36
+ const passed = Number(report.numPassedTests || 0);
37
+ const assertions = (report.testResults || []).flatMap((suite) => suite.assertionResults || []);
38
+ // Vitest 4 calls skipped assertions `skipped` while older JSON reporters called them
39
+ // `pending`; numPendingTests remains the aggregate count in both schemas.
40
+ const skippedTests = assertions.filter(({ status }) => status === 'skipped' || status === 'pending')
41
+ .map(({ fullName, title }) => ({ fullName: fullName || title, title }))
42
+ .filter(({ fullName, title }) => Boolean(fullName && title));
43
+ const todoTests = assertions.filter(({ status }) => status === 'todo')
44
+ .map(({ fullName, title }) => fullName || title)
45
+ .filter(Boolean);
46
+ const unknownSkips = skippedTests.filter(({ title }) => !ALLOWED_LINUX_SKIP_TITLES.has(title));
47
+ if (total <= 0 || failed !== 0 || passed + todo + skipped !== total
48
+ || skippedTests.length !== skipped || todoTests.length !== todo || unknownSkips.length > 0) {
49
+ const names = unknownSkips.map(({ fullName }) => fullName).join('; ');
50
+ throw new Error(`integration report is not an exact governed pass (${passed} passed, ${todo} todo, ${total} total, ${skipped} skipped; unknown skips: ${names || 'none'})`);
51
+ }
52
+ return {
53
+ schemaVersion: 1, kind: 'ruvnet-brain-integration-evidence', sourceSha,
54
+ workflow: 'integration-linux', runId: Number(runId), runAttempt: Number(runAttempt),
55
+ total, passed, failed: 0, skipped,
56
+ skippedTests: skippedTests.map(({ fullName }) => fullName).sort(),
57
+ todo, todoTests: todoTests.sort(), exclusionPolicy: EXCLUSION_POLICY,
58
+ exclusionsSha256: digestNames([
59
+ ...skippedTests.map(({ fullName }) => `skip:${fullName}`),
60
+ ...todoTests.map((name) => `todo:${name}`),
61
+ ]),
62
+ verdict: 'PASS',
63
+ };
64
+ }
65
+
66
+ const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
67
+ if (isCli) {
68
+ const value = (name) => process.argv[process.argv.indexOf(name) + 1];
69
+ const receipt = buildIntegrationEvidence(JSON.parse(fs.readFileSync(value('--report'), 'utf8')), {
70
+ sourceSha: value('--sha'), runId: value('--run-id'), runAttempt: value('--run-attempt'),
71
+ });
72
+ fs.writeFileSync(value('--out'), `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx' });
73
+ }
@@ -199,9 +199,10 @@ sh scripts/memdb-health.sh "$RUVNET_PROJECT_MEMORY_DB" >> "$LOG" 2>&1 \
199
199
  # maintainer away for a month. So the last step is handed to the nightly, which is already the thing
200
200
  # that runs unattended.
201
201
  #
202
- # It does NOT publish. self-update.mjs:56 refuses --publish and only protected-release.yml may ship;
203
- # this DISPATCHES that workflow, so every gate (exact-SHA evidence, clean worktree, release-proof,
204
- # host verification, post-publication seal) still runs exactly as designed. It stands down on any
202
+ # It does NOT publish or dispatch a publisher. The watchdog is report-only: release-candidate-
203
+ # preflight runs long qualification once on release/**, then protected-release imports the artifact
204
+ # by exact-SHA name, revalidates it, publishes, executes public 3x3, and appends install-verified.
205
+ # Change-triggered architecture/oracle review is evidence, not publication authority. It stands down on any
205
206
  # unexpected state — dirty tree, open PRs, no green exact-SHA evidence, a -dev version, a stalled
206
207
  # Actions plane — because a watchdog acting on a partial picture is worse than no watchdog.
207
208
  # ── GITHUB HEALTH (2026-08-08). Stuart: "You should never ever let it be in a situation where
@@ -460,11 +460,11 @@ export function resolveMemoryDb(projectDir) {
460
460
  })[0];
461
461
  }
462
462
 
463
- function probeMemory(projectDir) {
463
+ function probeMemory(projectDir, { now = Date.now() } = {}) {
464
464
  const db = resolveMemoryDb(projectDir);
465
465
  const probes = {};
466
466
  // compaction survival + session surfacing are filesystem facts, always checkable
467
- const snapshot = inspectSessionSnapshots(projectDir);
467
+ const snapshot = inspectSessionSnapshots(projectDir, { now });
468
468
  const snapshotDetail = snapshot.kind === 'canonical'
469
469
  ? 'a fresh versioned PreCompact snapshot is present in .swarm/agentdb-sessions.jsonl'
470
470
  : snapshot.kind === 'legacy'
@@ -508,7 +508,7 @@ function probeMemory(projectDir) {
508
508
 
509
509
  const cp = robustRead(db, "SELECT max(updated_at) FROM memory_entries WHERE key LIKE 'project-state-current%';");
510
510
  if (cp.ok && cp.value) {
511
- const ageH = (Date.now() - Number(cp.value) * (String(cp.value).length <= 10 ? 1000 : 1)) / 3.6e6;
511
+ const ageH = (now - Number(cp.value) * (String(cp.value).length <= 10 ? 1000 : 1)) / 3.6e6;
512
512
  probes.coverage = Number.isFinite(ageH) && ageH < 48
513
513
  ? { status: 'ok', detail: `project checkpoint present, ~${Math.max(0, ageH).toFixed(0)}h old` }
514
514
  : { status: 'warn', detail: 'project checkpoint present but stale (>2 days)' };
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { aggregateEvidence } from './release-evidence-aggregate.mjs';
7
+ import { canonicalJson } from './release-transaction.mjs';
8
+ import { EXCLUSION_POLICY } from './integration-evidence.mjs';
9
+
10
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
11
+
12
+ function readJson(file) {
13
+ const bytes = fs.readFileSync(file);
14
+ return { value: JSON.parse(bytes), digest: sha256(bytes) };
15
+ }
16
+
17
+ function requireExactSet(actual, expected, label) {
18
+ const got = [...actual].sort();
19
+ const want = [...expected].sort();
20
+ if (canonicalJson(got) !== canonicalJson(want)) {
21
+ throw new Error(`${label} differs: expected ${want.join(', ')}, got ${got.join(', ')}`);
22
+ }
23
+ }
24
+
25
+ function passLeaf({ name, sha, payloadId, source, receiptSha256, ...details }) {
26
+ const validated = /^[a-f0-9]{64}$/.test(receiptSha256 || '');
27
+ return {
28
+ name,
29
+ sha,
30
+ payloadId,
31
+ status: validated ? 'completed' : 'failed',
32
+ conclusion: validated ? 'success' : 'failure',
33
+ verdict: validated ? 'PASS' : 'FAIL',
34
+ source,
35
+ receiptSha256,
36
+ ...details,
37
+ };
38
+ }
39
+
40
+ export function buildPrepublicationEvidence({
41
+ sha,
42
+ version,
43
+ runId,
44
+ manifestFile,
45
+ payloadProofFile,
46
+ hostFile,
47
+ ciFile,
48
+ integrationFile,
49
+ uxFiles,
50
+ strangerFile,
51
+ }) {
52
+ if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^\d+\.\d+\.\d+$/.test(version || '') || !Number.isSafeInteger(runId) || runId <= 0) {
53
+ throw new Error('prepublication identity is malformed');
54
+ }
55
+
56
+ const manifestBytes = fs.readFileSync(manifestFile);
57
+ const manifest = JSON.parse(manifestBytes);
58
+ const payload = JSON.parse(fs.readFileSync(payloadProofFile));
59
+ if (manifest.candidateSha !== sha || manifest.version !== version || manifest.tag !== `v${version}`) {
60
+ throw new Error('candidate manifest identity mismatch');
61
+ }
62
+ if (!/^[a-f0-9]{64}$/.test(payload.payloadId || '')) throw new Error('payload identity is malformed');
63
+
64
+ const host = readJson(hostFile);
65
+ if (host.value.schemaVersion !== 1 || host.value.sha !== sha || host.value.payloadId !== payload.payloadId
66
+ || !/^[a-f0-9]{64}$/.test(host.value.artifactSha256 || '') || host.value.leaves?.length !== 3) {
67
+ throw new Error('candidate host evidence identity or shape mismatch');
68
+ }
69
+ requireExactSet(host.value.leaves.map(({ name }) => name), ['claude-only', 'codex-only', 'dual-host'], 'candidate host leaves');
70
+ for (const leaf of host.value.leaves) {
71
+ const grounding = leaf.grounding;
72
+ const grounded = grounding && ['repo', 'path', 'file', 'storedPath']
73
+ .every((field) => typeof grounding[field] === 'string' && grounding[field].trim());
74
+ if (leaf.sha !== sha || leaf.payloadId !== payload.payloadId || leaf.status !== 'completed'
75
+ || leaf.conclusion !== 'success' || leaf.verdict !== 'PASS'
76
+ || leaf.functionalSearch !== true || leaf.searchExit !== 0 || !grounded
77
+ || leaf.artifactSha256 !== host.value.artifactSha256) {
78
+ throw new Error(`candidate host leaf is not an exact PASS: ${leaf.name || '(missing)'}`);
79
+ }
80
+ }
81
+
82
+ const ci = readJson(ciFile);
83
+ const requiredCiJobs = ['candidate-preflight', 'check', 'windows-unit', 'warm-brain', 'release-qe'];
84
+ if (ci.value.schemaVersion !== 1 || ci.value.kind !== 'ruvnet-brain-candidate-ci-evidence'
85
+ || ci.value.sourceSha !== sha || ci.value.version !== version || ci.value.payloadId !== payload.payloadId
86
+ || ci.value.payloadManifestSha256 !== sha256(manifestBytes) || ci.value.workflow !== 'ci'
87
+ || ci.value.runId !== runId || ci.value.verdict !== 'PASS' || ci.value.skipped !== 0 || ci.value.unknown !== 0) {
88
+ throw new Error('candidate CI receipt identity or verdict mismatch');
89
+ }
90
+ requireExactSet(ci.value.jobs?.map(({ name }) => name) || [], requiredCiJobs, 'candidate CI jobs');
91
+ if (ci.value.jobs.some(({ conclusion }) => conclusion !== 'success')) throw new Error('candidate CI receipt contains a non-success job');
92
+
93
+ const integration = readJson(integrationFile);
94
+ if (integration.value.schemaVersion !== 1 || integration.value.kind !== 'ruvnet-brain-integration-evidence'
95
+ || integration.value.sourceSha !== sha || integration.value.workflow !== 'integration-linux'
96
+ || integration.value.runId !== runId || integration.value.verdict !== 'PASS'
97
+ || integration.value.total <= 0
98
+ || integration.value.passed + Number(integration.value.todo || 0) + Number(integration.value.skipped || 0) !== integration.value.total
99
+ || integration.value.failed !== 0
100
+ || !Array.isArray(integration.value.skippedTests)
101
+ || integration.value.skippedTests.length !== integration.value.skipped
102
+ || integration.value.skippedTests.some((name) => typeof name !== 'string' || !name.trim())) {
103
+ throw new Error('integration receipt is not an exact, fully accounted PASS');
104
+ }
105
+ if (integration.value.exclusionPolicy !== EXCLUSION_POLICY
106
+ || !Array.isArray(integration.value.todoTests)
107
+ || integration.value.todoTests.length !== Number(integration.value.todo || 0)
108
+ || !/^[a-f0-9]{64}$/.test(integration.value.exclusionsSha256 || '')) {
109
+ throw new Error('integration receipt exclusions are not governed and fully enumerated');
110
+ }
111
+
112
+ if (!Array.isArray(uxFiles) || uxFiles.length !== 3) throw new Error('exactly three UX receipts are required');
113
+ const ux = uxFiles.map(readJson);
114
+ requireExactSet(ux.map(({ value }) => value.platform), ['darwin', 'linux', 'win32'], 'UX platforms');
115
+ for (const receipt of ux) {
116
+ const value = receipt.value;
117
+ if (value.schemaVersion !== 1 || value.suite !== 'ruvnet-brain-ux-qe' || value.gitSha !== sha
118
+ || value.pass !== true || !Array.isArray(value.hardFailures) || value.hardFailures.length !== 0) {
119
+ throw new Error(`UX receipt is not an exact PASS: ${value.platform || '(missing)'}`);
120
+ }
121
+ }
122
+ const uxDigest = sha256(Buffer.from(canonicalJson(ux.map(({ value, digest }) => ({ platform: value.platform, digest }))
123
+ .sort((a, b) => a.platform.localeCompare(b.platform)))));
124
+
125
+ const stranger = readJson(strangerFile);
126
+ const strangerJobs = ['ubuntu', 'macos', 'windows-gitbash', 'windows-powershell', 'hostile'];
127
+ if (stranger.value.schemaVersion !== 1 || stranger.value.sha !== sha || stranger.value.payloadId !== payload.payloadId
128
+ || Number(stranger.value.sourceCiRunId) !== runId || Number(stranger.value.strangerRunId) !== runId
129
+ || stranger.value.verdict !== 'PASS') {
130
+ throw new Error('stranger receipt identity or verdict mismatch');
131
+ }
132
+ requireExactSet(stranger.value.jobs || [], strangerJobs, 'stranger jobs');
133
+
134
+ const common = { sha, payloadId: payload.payloadId, runId };
135
+ const leaves = [
136
+ passLeaf({ ...common, name: 'source-quality', source: 'candidate-ci-receipt:check', receiptSha256: ci.digest }),
137
+ passLeaf({ ...common, name: 'ux-qe', source: 'ux-qe-receipts:darwin,linux,win32', receiptSha256: uxDigest }),
138
+ passLeaf({ ...common, name: 'release-qe', source: 'candidate-ci-receipt:release-qe', receiptSha256: ci.digest }),
139
+ passLeaf({ ...common, name: 'integration-linux', source: 'integration-receipt', receiptSha256: integration.digest,
140
+ skipped: integration.value.skipped, todo: integration.value.todo }),
141
+ passLeaf({ ...common, name: 'stranger-linux', source: 'stranger-receipt:ubuntu', receiptSha256: stranger.digest }),
142
+ passLeaf({ ...common, name: 'stranger-macos', source: 'stranger-receipt:macos', receiptSha256: stranger.digest }),
143
+ passLeaf({ ...common, name: 'stranger-windows', source: 'stranger-receipt:windows', receiptSha256: stranger.digest }),
144
+ ...host.value.leaves.map((leaf) => ({ ...leaf, runId, receiptSha256: host.digest })),
145
+ ];
146
+ return { leaves, envelope: aggregateEvidence({ sha, payloadId: payload.payloadId, leaves }) };
147
+ }
148
+
149
+ const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
150
+ if (isCli) {
151
+ const arg = (name) => {
152
+ const index = process.argv.indexOf(name);
153
+ return index >= 0 ? process.argv[index + 1] : null;
154
+ };
155
+ const uxDir = path.resolve(arg('--ux-dir'));
156
+ const uxFiles = fs.readdirSync(uxDir).filter((name) => name.endsWith('.json')).sort().map((name) => path.join(uxDir, name));
157
+ const result = buildPrepublicationEvidence({
158
+ sha: arg('--sha'),
159
+ version: arg('--version'),
160
+ runId: Number(arg('--run-id')),
161
+ manifestFile: path.resolve(arg('--manifest')),
162
+ payloadProofFile: path.resolve(arg('--payload-proof')),
163
+ hostFile: path.resolve(arg('--hosts')),
164
+ ciFile: path.resolve(arg('--ci')),
165
+ integrationFile: path.resolve(arg('--integration')),
166
+ uxFiles,
167
+ strangerFile: path.resolve(arg('--stranger')),
168
+ });
169
+ const leavesOut = path.resolve(arg('--leaves-out'));
170
+ const envelopeOut = path.resolve(arg('--envelope-out'));
171
+ fs.writeFileSync(leavesOut, `${JSON.stringify(result.leaves, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
172
+ fs.writeFileSync(envelopeOut, `${canonicalJson(result.envelope)}\n`, { flag: 'wx', mode: 0o600 });
173
+ process.stdout.write(`${JSON.stringify({ verdict: result.envelope.verdict, evidenceDigest: result.envelope.evidenceDigest })}\n`);
174
+ }
@@ -4,6 +4,7 @@
4
4
  import crypto from 'node:crypto';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
7
8
  import { evaluateCandidateReceipt } from './release-proof.mjs';
8
9
  import { getVersion } from './version.mjs';
9
10
 
@@ -13,12 +14,27 @@ export const PROTECTED_VERSION = getVersion();
13
14
  const hex = (value, length) => new RegExp(`^[0-9a-f]{${length}}$`).test(String(value || ''));
14
15
  const digestOf = (receipt) => String(receipt?.artifact?.sha256 || '').replace(/^sha256:/, '');
15
16
 
16
- export function validateProtectedPublishInvocation({ root = process.cwd(), env = process.env } = {}) {
17
+ export function validatePublishableVersion(version) {
18
+ return /^\d+\.\d+\.\d+$/.test(String(version || ''))
19
+ ? []
20
+ : ['development versions cannot be published'];
21
+ }
22
+
23
+ export function validateProtectedPublishEnvironment(env = process.env) {
17
24
  const failures = [];
18
- let releaseMode = 'unknown';
19
- if (env.GITHUB_ACTIONS !== 'true' || env.GITHUB_WORKFLOW !== PROTECTED_WORKFLOW) {
20
- failures.push('publication is allowed only inside the protected-release GitHub workflow');
25
+ if (env.GITHUB_ACTIONS !== 'true'
26
+ || env.GITHUB_WORKFLOW !== PROTECTED_WORKFLOW
27
+ || env.GITHUB_REPOSITORY !== 'stuinfla/ruvnet-brain'
28
+ || env.GITHUB_EVENT_NAME !== 'workflow_dispatch'
29
+ || env.GITHUB_REF_PROTECTED !== 'true') {
30
+ failures.push('publication is allowed only inside the protected-release GitHub workflow on a protected workflow_dispatch ref');
21
31
  }
32
+ return failures;
33
+ }
34
+
35
+ export function validateProtectedPublishInvocation({ root = process.cwd(), env = process.env } = {}) {
36
+ const failures = [...validateProtectedPublishEnvironment(env)];
37
+ let releaseMode = 'unknown';
22
38
 
23
39
  const expectedSha = String(env.RUVNET_EXPECTED_SHA || '');
24
40
  const expectedDigest = String(env.RUVNET_EXPECTED_ARTIFACT_SHA256 || '').replace(/^sha256:/, '');
@@ -26,6 +42,7 @@ export function validateProtectedPublishInvocation({ root = process.cwd(), env =
26
42
  if (!hex(expectedSha, 40)) failures.push('expected candidate SHA is missing or malformed');
27
43
  if (!hex(expectedDigest, 64)) failures.push('expected artifact digest is missing or malformed');
28
44
  if (expectedVersion !== PROTECTED_VERSION) failures.push(`release generation must be ${PROTECTED_VERSION}`);
45
+ failures.push(...validatePublishableVersion(expectedVersion));
29
46
 
30
47
  const receiptRelative = String(env.RUVNET_CANDIDATE_RECEIPT || '');
31
48
  const evidenceRoot = path.resolve(root, 'release-evidence');
@@ -74,3 +91,14 @@ export function validateProtectedPublishInvocation({ root = process.cwd(), env =
74
91
  failures,
75
92
  };
76
93
  }
94
+
95
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
96
+ const result = validateProtectedPublishInvocation();
97
+ if (result.verdict !== 'PASS') {
98
+ console.error('[protected-release] publication refused:');
99
+ for (const failure of result.failures) console.error(` - ${failure}`);
100
+ process.exitCode = 1;
101
+ } else {
102
+ console.log('[protected-release] exact candidate publication authority verified');
103
+ }
104
+ }