ruvnet-brain 4.3.9 → 4.3.11
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.
- package/README.md +26 -9
- package/bin/install.mjs +646 -366
- package/bin/nightly-refresh.mjs +115 -0
- package/kb/brain-profile.mjs +118 -32
- package/kb/lifecycle-evidence-retention.mjs +237 -0
- package/kb/refresh-run.mjs +367 -0
- package/kb/retrieval-result.mjs +39 -0
- package/kb/update-storage-transaction.mjs +439 -0
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +2 -165
- package/plugin/hooks/hook-contracts.json +4 -65
- package/plugin/hooks/hooks.json +2 -208
- package/plugin/scripts/adr-currency-gate.mjs +29 -19
- package/plugin/scripts/capability-registry.mjs +10 -83
- package/plugin/scripts/codex-hook-adapter.mjs +62 -9
- package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
- package/plugin/scripts/continuation-gate.mjs +30 -28
- package/plugin/scripts/continuation-objective.mjs +34 -0
- package/plugin/scripts/development-maintenance.mjs +49 -0
- package/plugin/scripts/hook-shim.mjs +3 -0
- package/plugin/scripts/learn-flush.mjs +6 -1
- package/plugin/scripts/lesson-gate.mjs +5 -2
- package/plugin/scripts/lesson-presentation.mjs +5 -0
- package/plugin/scripts/md-stamp.mjs +109 -6
- package/plugin/scripts/memory-doctor.mjs +3 -7
- package/plugin/scripts/nightly-controller.mjs +9 -27
- package/plugin/scripts/nightly-scheduler.mjs +494 -0
- package/plugin/scripts/project-progression-contract.mjs +4 -4
- package/plugin/scripts/project-progression-store.mjs +35 -6
- package/plugin/scripts/ruflo-bin.mjs +20 -0
- package/plugin/scripts/session-snapshot-contract.mjs +6 -1
- package/plugin/scripts/version-bump-gate.sh +14 -2
- package/plugin/skills/release-proof/SKILL.md +6 -2
- package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
- package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
- package/scripts/build-bundle.mjs +8 -0
- package/scripts/build-primer.mjs +2 -3
- package/scripts/candidate-host-evidence.mjs +59 -41
- package/scripts/ci/build-fixture-kb.mjs +59 -2
- package/scripts/ci/mutate-hook-timeout.mjs +24 -47
- package/scripts/ci/stranger-scenario.mjs +13 -28
- package/scripts/claims-verify.mjs +219 -21
- package/scripts/console-engine.mjs +4 -18
- package/scripts/console-runtime-identity.mjs +6 -0
- package/scripts/development-maintenance.mjs +47 -0
- package/scripts/development-push-check.mjs +35 -0
- package/scripts/distill-project.mjs +24 -47
- package/scripts/doc-currency.mjs +91 -19
- package/scripts/git-hooks/pre-push +4 -162
- package/scripts/health-repair.mjs +22 -4
- package/scripts/hook-retirement-check.mjs +22 -0
- package/scripts/host-install-matrix.mjs +125 -61
- package/scripts/integration-evidence.mjs +11 -6
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/nightly-package-observation.mjs +67 -0
- package/scripts/nightly-two-run-proof.mjs +489 -0
- package/scripts/nightly-watchdog.mjs +17 -2
- package/scripts/npm-invocation.mjs +20 -0
- package/scripts/prepublication-evidence.mjs +42 -6
- package/scripts/primer-grounding.mjs +20 -0
- package/scripts/product-integrity-contract.mjs +22 -2
- package/scripts/public-verification-abandon.mjs +101 -0
- package/scripts/public-verification-aggregate.mjs +61 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +64 -8
- package/scripts/publication-receipt.mjs +156 -50
- package/scripts/published-surface-probe.mjs +10 -2
- package/scripts/qa-contract.mjs +57 -0
- package/scripts/qa-lanes.mjs +40 -0
- package/scripts/qa-runner.mjs +54 -64
- package/scripts/qe/ux-suite.mjs +10 -34
- package/scripts/qualified-candidate-check.mjs +124 -0
- package/scripts/release-abort-stale.mjs +2 -2
- package/scripts/release-projection.mjs +20 -1
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/remedy-registry.mjs +6 -13
- package/scripts/retrieval-canary.mjs +91 -18
- package/scripts/selfcheck.mjs +12 -4
- package/scripts/snapshot-freshness.mjs +58 -0
- package/scripts/stack-sync.mjs +41 -43
- package/scripts/staged-host-verifier.mjs +63 -14
- package/scripts/wired-check.mjs +31 -2
|
@@ -27,6 +27,8 @@ import os from 'node:os';
|
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import { spawn, spawnSync } from 'node:child_process';
|
|
29
29
|
import { pathToFileURL } from 'node:url';
|
|
30
|
+
import { parseRetrievalResult } from '../kb/retrieval-result.mjs';
|
|
31
|
+
import { runRetrievalCanaries, validateRetrievalCanaryReceipt, resolveInstalledCanaryCitation } from './retrieval-canary.mjs';
|
|
30
32
|
|
|
31
33
|
/** The three host shapes a release must survive. ONE name each, for every consumer. */
|
|
32
34
|
export const HOST_MODES = Object.freeze(['claude', 'codex', 'dual']);
|
|
@@ -125,7 +127,7 @@ export function fixturePath(mode, temp, locate) {
|
|
|
125
127
|
export function runHostMatrix({ packageRoot, version, variant = 'staged', locate, temp, run = spawnSync }) {
|
|
126
128
|
const spec = VARIANTS[variant];
|
|
127
129
|
if (!spec) throw new Error(`unknown host-matrix variant: ${variant}`);
|
|
128
|
-
const workspace = temp || fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-host-matrix-'));
|
|
130
|
+
const workspace = fs.realpathSync(temp || fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-host-matrix-')));
|
|
129
131
|
const installer = path.join(packageRoot, 'bin', 'install.mjs');
|
|
130
132
|
const fixtures = {};
|
|
131
133
|
let verdict = 'PASS';
|
|
@@ -201,10 +203,11 @@ export async function runHostMatrixAsync({
|
|
|
201
203
|
runMcpSearch = runInstalledMcpSearch,
|
|
202
204
|
verifyGrounding = verifyInstalledGrounding,
|
|
203
205
|
resolveMcpServer = resolveInstalledMcpServer,
|
|
206
|
+
retrieval,
|
|
204
207
|
}) {
|
|
205
208
|
const spec = VARIANTS[variant];
|
|
206
209
|
if (!spec) throw new Error(`unknown host-matrix variant: ${variant}`);
|
|
207
|
-
const workspace = temp || fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-host-matrix-'));
|
|
210
|
+
const workspace = fs.realpathSync(temp || fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-host-matrix-')));
|
|
208
211
|
const sharedModelCache = path.join(workspace, 'models-cache');
|
|
209
212
|
fs.mkdirSync(sharedModelCache, { recursive: true });
|
|
210
213
|
const contexts = HOST_MODES.map((mode) => {
|
|
@@ -261,21 +264,43 @@ export async function runHostMatrixAsync({
|
|
|
261
264
|
}
|
|
262
265
|
|
|
263
266
|
const searches = await Promise.all(contexts.map(async (context) => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
let session;
|
|
268
|
+
try {
|
|
269
|
+
const serverPath = resolveMcpServer(context);
|
|
270
|
+
// One installed worker per host: model/store state survives smoke and every sealed case.
|
|
271
|
+
session = runMcpSearch === runInstalledMcpSearch ? createInstalledMcpSession({ serverPath, env: context.env }) : null;
|
|
272
|
+
const searchMcp = session ? (args) => session.search(args) : runMcpSearch;
|
|
273
|
+
const processResult = await searchMcp({ mode: context.mode, serverPath, env: context.env });
|
|
274
|
+
const output = `${processResult.stdout || ''}${processResult.stderr || ''}`;
|
|
275
|
+
if (processResult.error || processResult.status !== 0) {
|
|
276
|
+
return { context, processResult, error: `MCP search failed for ${context.mode} (${processDiagnostic(processResult)}): ${output.slice(-2000)}` };
|
|
277
|
+
}
|
|
278
|
+
const grounding = await verifyGrounding(output, context.env.RUVNET_BRAIN_KB);
|
|
279
|
+
if (!grounding?.grounded) return { context, processResult, error: `MCP search grounding unproven for ${context.mode}` };
|
|
280
|
+
let receipt;
|
|
281
|
+
if (retrieval) {
|
|
282
|
+
receipt = await runRetrievalCanaries({ ...retrieval,
|
|
283
|
+
search: async ({ query, k }) => {
|
|
284
|
+
const result = await searchMcp({ mode: context.mode, serverPath, env: context.env, query, k });
|
|
285
|
+
if (result.error || result.status !== 0) throw new Error(`canary MCP search failed: ${processDiagnostic(result)}`);
|
|
286
|
+
return parseRetrievalResult(result.mcpResult, { query, k });
|
|
287
|
+
},
|
|
288
|
+
citationResolver: (matched, expected) => resolveInstalledCanaryCitation({ kbDir: context.env.RUVNET_BRAIN_KB, matched, expected }),
|
|
289
|
+
});
|
|
290
|
+
try { validateRetrievalCanaryReceipt(receipt, { plan: retrieval.plan }); }
|
|
291
|
+
catch (error) { return { context, processResult, grounding, retrieval: receipt, error: `${context.mode} canary rejected: ${error.message}` }; }
|
|
292
|
+
}
|
|
293
|
+
return { context, processResult, grounding, ...(receipt ? { retrieval: receipt } : {}) };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return { context, processResult: { status: null, error }, error: `${context.mode} host search failed: ${error.message}` };
|
|
296
|
+
} finally { await session?.close(); }
|
|
273
297
|
}));
|
|
274
|
-
const fixtures = Object.fromEntries(searches.map(({ context, processResult, grounding, error }) => [context.mode, {
|
|
298
|
+
const fixtures = Object.fromEntries(searches.map(({ context, processResult, grounding, retrieval, error }) => [context.mode, {
|
|
275
299
|
status: error ? 'FAIL' : 'PASS',
|
|
276
300
|
version,
|
|
277
301
|
process: processIdentity(processResult),
|
|
278
302
|
...(grounding?.receipt ? { grounding: grounding.receipt } : {}),
|
|
303
|
+
...(retrieval ? { retrieval } : {}),
|
|
279
304
|
...(error ? { error } : {}),
|
|
280
305
|
}]));
|
|
281
306
|
const error = searches.find((result) => result.error)?.error;
|
|
@@ -318,56 +343,95 @@ async function verifyInstalledGrounding(output, kbDir) {
|
|
|
318
343
|
return verifyGrounding(output, kbDir);
|
|
319
344
|
}
|
|
320
345
|
|
|
321
|
-
function
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
346
|
+
export function createInstalledMcpSession({ serverPath, env, timeout = 300_000, shutdownTimeout = 5000 }) {
|
|
347
|
+
const child = spawn(process.execPath, [serverPath], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
348
|
+
// Decode across chunk boundaries so split UTF-8 cannot corrupt content-bound citations.
|
|
349
|
+
child.stdout.setEncoding('utf8');
|
|
350
|
+
child.stderr.setEncoding('utf8');
|
|
351
|
+
const pending = new Map();
|
|
352
|
+
let buffer = '', stderr = '', nextId = 0, initialized = false;
|
|
353
|
+
let terminalError = null, closePromise, exited = false, exitSignal = null;
|
|
354
|
+
let queue = Promise.resolve();
|
|
355
|
+
let resolveExit;
|
|
356
|
+
const exit = new Promise((resolve) => { resolveExit = resolve; });
|
|
357
|
+
const close = (error = new Error('MCP session closed')) => {
|
|
358
|
+
if (closePromise) return closePromise;
|
|
359
|
+
terminalError ||= error;
|
|
360
|
+
for (const { reject } of pending.values()) reject(terminalError);
|
|
361
|
+
pending.clear();
|
|
362
|
+
closePromise = (async () => {
|
|
363
|
+
if (exited) return;
|
|
364
|
+
// EOF lets the installed proxy await its worker shutdown on Windows as well as POSIX.
|
|
365
|
+
child.stdin.end();
|
|
366
|
+
const escalation = setTimeout(() => { if (!exited) child.kill('SIGKILL'); }, shutdownTimeout);
|
|
367
|
+
try { await exit; } finally { clearTimeout(escalation); }
|
|
368
|
+
})();
|
|
369
|
+
return closePromise;
|
|
370
|
+
};
|
|
371
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
372
|
+
child.stdout.on('data', (chunk) => {
|
|
373
|
+
buffer += chunk;
|
|
374
|
+
for (;;) {
|
|
375
|
+
const newline = buffer.indexOf('\n');
|
|
376
|
+
if (newline < 0) break;
|
|
377
|
+
const line = buffer.slice(0, newline).trim();
|
|
378
|
+
buffer = buffer.slice(newline + 1);
|
|
379
|
+
if (!line) continue;
|
|
380
|
+
let message;
|
|
381
|
+
try { message = JSON.parse(line); } catch { continue; }
|
|
382
|
+
const handler = pending.get(message.id);
|
|
383
|
+
if (handler) { pending.delete(message.id); handler.resolve(message); }
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
child.on('error', (error) => { void close(error); });
|
|
387
|
+
child.stdin.on('error', (error) => { void close(error); });
|
|
388
|
+
child.on('close', (status, signal) => {
|
|
389
|
+
exited = true; exitSignal = signal; resolveExit();
|
|
390
|
+
void close(new Error(`MCP server exited ${status}`));
|
|
391
|
+
});
|
|
392
|
+
const call = (method, params = {}) => new Promise((resolve, reject) => {
|
|
393
|
+
if (terminalError) { reject(terminalError); return; }
|
|
394
|
+
const id = ++nextId;
|
|
395
|
+
pending.set(id, { resolve, reject });
|
|
396
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
370
397
|
});
|
|
398
|
+
const execute = async ({ query = 'How does RuvNet Brain prove a public release artifact?', k = 5 } = {}) => {
|
|
399
|
+
if (terminalError) return { status: null, error: terminalError, signal: exitSignal, stdout: '', stderr };
|
|
400
|
+
stderr = '';
|
|
401
|
+
const timer = setTimeout(() => {
|
|
402
|
+
void close(Object.assign(new Error('MCP search timed out'), { code: 'ETIMEDOUT' }));
|
|
403
|
+
}, timeout);
|
|
404
|
+
try {
|
|
405
|
+
if (!initialized) {
|
|
406
|
+
const ready = await call('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'release-host-matrix', version: '1' } });
|
|
407
|
+
if (ready.error) throw new Error(`MCP initialize failed: ${JSON.stringify(ready.error)}`);
|
|
408
|
+
const listed = await call('tools/list');
|
|
409
|
+
if (!listed.result?.tools?.some((tool) => tool.name === 'search_ruvnet')) throw new Error('installed MCP does not advertise search_ruvnet');
|
|
410
|
+
initialized = true;
|
|
411
|
+
}
|
|
412
|
+
const searched = await call('tools/call', { name: 'search_ruvnet', arguments: { query, k } });
|
|
413
|
+
const stdout = (searched.result?.content || []).map((item) => item.text || '').join('\n');
|
|
414
|
+
if (searched.error || searched.result?.isError) throw new Error(`installed Brain search failed: ${stdout.slice(0, 400)}`);
|
|
415
|
+
return { status: 0, signal: null, error: null, stdout, stderr, mcpResult: searched.result };
|
|
416
|
+
} catch (error) {
|
|
417
|
+
await close(error);
|
|
418
|
+
return { status: null, signal: exitSignal, error: terminalError, stdout: '', stderr };
|
|
419
|
+
} finally { clearTimeout(timer); }
|
|
420
|
+
};
|
|
421
|
+
return {
|
|
422
|
+
// The scorer may schedule concurrent cases; only one reaches this model worker at a time.
|
|
423
|
+
search(args) {
|
|
424
|
+
const result = queue.then(() => execute(args));
|
|
425
|
+
queue = result.then(() => undefined, () => undefined);
|
|
426
|
+
return result;
|
|
427
|
+
},
|
|
428
|
+
close,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function runInstalledMcpSearch(options) {
|
|
433
|
+
const session = createInstalledMcpSession(options);
|
|
434
|
+
try { return await session.search(options); } finally { await session.close(); }
|
|
371
435
|
}
|
|
372
436
|
|
|
373
437
|
function spawnCommand(command, args, options) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import { validateQualificationReceipt } from './release-qualification.mjs';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -8,11 +9,6 @@ export const EXCLUSION_POLICY = 'release-linux-v1';
|
|
|
8
9
|
|
|
9
10
|
const ALLOWED_LINUX_SKIP_TITLES = new Set([
|
|
10
11
|
'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
12
|
'bridges, ranks, and is delivered as additionalContext at the write-code decision point',
|
|
17
13
|
'TEETH: an UNTAGGED row travels no further than the store — the chain is genuinely load-bearing',
|
|
18
14
|
'TEETH: a lesson tagged for a DIFFERENT moment does not fire at this one',
|
|
@@ -63,10 +59,19 @@ export function buildIntegrationEvidence(report, { sourceSha, runId, runAttempt
|
|
|
63
59
|
};
|
|
64
60
|
}
|
|
65
61
|
|
|
62
|
+
export const RELEASE_QUALIFICATION_POLICY = 'reviewed-release-integration-v1';
|
|
63
|
+
export function buildQualifiedIntegrationEvidence(report, identity) {
|
|
64
|
+
validateQualificationReceipt(report, { suite: 'integration', platform: 'linux', sourceSha: identity.sourceSha });
|
|
65
|
+
const evidence = buildIntegrationEvidence(report.testReport, identity);
|
|
66
|
+
return { ...evidence, exclusionPolicy: RELEASE_QUALIFICATION_POLICY,
|
|
67
|
+
qualificationReceiptSha256: report.receiptSha256, requirements: report.requirements };
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
67
71
|
if (isCli) {
|
|
68
72
|
const value = (name) => process.argv[process.argv.indexOf(name) + 1];
|
|
69
|
-
const
|
|
73
|
+
const build = process.argv.includes('--qualified') ? buildQualifiedIntegrationEvidence : buildIntegrationEvidence;
|
|
74
|
+
const receipt = build(JSON.parse(fs.readFileSync(value('--report'), 'utf8')), {
|
|
70
75
|
sourceSha: value('--sha'), runId: value('--run-id'), runAttempt: value('--run-attempt'),
|
|
71
76
|
});
|
|
72
77
|
fs.writeFileSync(value('--out'), `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx' });
|
|
@@ -246,10 +246,30 @@ function writeSettings(file, { dirs, stateDir, attemptsFile }) {
|
|
|
246
246
|
return file;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
// The product plugin may intentionally be disabled. Register only this replay's two hooks
|
|
250
|
+
// in the invocation layer; never edit user config or depend on an ambient installed manifest.
|
|
251
|
+
export function codexReplayHookArgs(brainHome) {
|
|
252
|
+
if (!brainHome || !path.isAbsolute(brainHome)) throw new Error('explicit absolute replay Brain home is required');
|
|
253
|
+
const quote = (value) => "'" + String(value).replaceAll("'", "'\"'\"'") + "'";
|
|
254
|
+
const wrapper = `${quote(process.execPath)} ${quote(path.join(brainHome, 'codex-hook.mjs'))}`;
|
|
255
|
+
const hook = (event, suffix, matcher = '') => `hooks.${event}=[{${matcher}hooks=[{type="command",command=${JSON.stringify(`${wrapper} unprompted-speech ${suffix}`)},timeout=20}]}]`;
|
|
256
|
+
return ['-c', hook('UserPromptSubmit', 'UserPromptSubmit'),
|
|
257
|
+
'-c', hook('PreToolUse', 'PreToolUse-bash', 'matcher=".*",')];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function codexReplayInstrumentationError(events, sequence, attempts) {
|
|
261
|
+
if (attempts.length && sequence.some((event) => event.kind === 'tool')) return null;
|
|
262
|
+
const executed = events.filter((event) => event.type === 'item.completed'
|
|
263
|
+
&& event.item?.type === 'command_execution').length;
|
|
264
|
+
return `Codex replay recorder did not observe a tool invocation (${executed} command execution event(s) in the native stream); `
|
|
265
|
+
+ 'fixture hook instrumentation is missing, so command absence and learning cannot be graded';
|
|
266
|
+
}
|
|
267
|
+
|
|
249
268
|
export function buildCodexArgv({
|
|
250
269
|
model = 'gpt-5.6-sol',
|
|
251
270
|
prompt = REPLAY_PROMPT,
|
|
252
271
|
appendSystemPrompt = null,
|
|
272
|
+
brainHome,
|
|
253
273
|
} = {}) {
|
|
254
274
|
const fullPrompt = appendSystemPrompt ? `${appendSystemPrompt}\n\n${prompt}` : prompt;
|
|
255
275
|
return [
|
|
@@ -257,6 +277,7 @@ export function buildCodexArgv({
|
|
|
257
277
|
'--ignore-rules', '--dangerously-bypass-hook-trust', '-m', model,
|
|
258
278
|
'-c', 'model_reasoning_effort="low"',
|
|
259
279
|
'-c', 'shell_environment_policy.inherit="all"',
|
|
280
|
+
...codexReplayHookArgs(brainHome),
|
|
260
281
|
fullPrompt,
|
|
261
282
|
];
|
|
262
283
|
}
|
|
@@ -312,7 +333,7 @@ export function runArm({
|
|
|
312
333
|
let argv;
|
|
313
334
|
if (host === 'codex') {
|
|
314
335
|
binary = CODEX_BIN;
|
|
315
|
-
argv = buildCodexArgv({ model, prompt: spec.prompt, appendSystemPrompt });
|
|
336
|
+
argv = buildCodexArgv({ model, prompt: spec.prompt, appendSystemPrompt, brainHome: dirs.brainHome });
|
|
316
337
|
} else {
|
|
317
338
|
const settings = writeSettings(path.join(dirs.base, `settings-${tag}.json`), {
|
|
318
339
|
dirs,
|
|
@@ -410,7 +431,7 @@ export function runArm({
|
|
|
410
431
|
transcript: path.relative(ROOT, streamFile),
|
|
411
432
|
exit: result.status,
|
|
412
433
|
spawnError: host === 'codex'
|
|
413
|
-
? parseCodexRunError(events, result)
|
|
434
|
+
? parseCodexRunError(events, result) || codexReplayInstrumentationError(events, sequence, attemptLines)
|
|
414
435
|
: replayRunError(events, result),
|
|
415
436
|
};
|
|
416
437
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
6
|
+
|
|
7
|
+
// npm dependencies are separately installed outside the immutable package payload.
|
|
8
|
+
export function packageTreeObservation(root) {
|
|
9
|
+
const files = [];
|
|
10
|
+
const visit = (dir, prefix = '') => {
|
|
11
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
12
|
+
if (!prefix && name === 'node_modules') continue;
|
|
13
|
+
const file = path.join(dir, name);
|
|
14
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
15
|
+
const stat = fs.lstatSync(file);
|
|
16
|
+
if (stat.isDirectory()) visit(file, relative);
|
|
17
|
+
else if (stat.isFile()) files.push({ path: relative, sha256: hash(fs.readFileSync(file)) });
|
|
18
|
+
else throw new Error(`npm package contains an unsupported entry: ${relative}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
visit(root);
|
|
22
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
23
|
+
return { name: manifest.name, version: manifest.version, fileCount: files.length,
|
|
24
|
+
treeSha256: hash(JSON.stringify(files)) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function observeNpxPackage(npmCache, expected) {
|
|
28
|
+
const base = path.join(npmCache, '_npx');
|
|
29
|
+
const roots = fs.existsSync(base) ? fs.readdirSync(base).map((name) =>
|
|
30
|
+
path.join(base, name, 'node_modules', 'ruvnet-brain')).filter((root) => fs.existsSync(root)) : [];
|
|
31
|
+
if (roots.length !== 1) throw new Error(`expected exactly one isolated npx Brain package, found ${roots.length}`);
|
|
32
|
+
const observed = packageTreeObservation(roots[0]);
|
|
33
|
+
if (JSON.stringify(observed) !== JSON.stringify(expected)) throw new Error('npx package bytes differ from verified public package');
|
|
34
|
+
return { ...observed, path: roots[0], observedAt: new Date().toISOString() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateNpxObservations(proof) {
|
|
38
|
+
const expected = proof.packageExecution?.expected;
|
|
39
|
+
const observations = proof.packageExecution?.observations;
|
|
40
|
+
if (proof.packageExecution?.policy !== 'production-latest-exact-cache-v1'
|
|
41
|
+
|| proof.registration?.packageTarget?.spec !== 'ruvnet-brain@latest'
|
|
42
|
+
|| proof.registration.packageTarget.sha256 !== null
|
|
43
|
+
|| expected?.packageSha256 !== proof.candidate?.sha256
|
|
44
|
+
|| expected?.name !== 'ruvnet-brain' || expected.version !== proof.candidate?.version
|
|
45
|
+
|| !/^[a-f0-9]{64}$/.test(expected.treeSha256 || '') || !Number.isSafeInteger(expected.fileCount)
|
|
46
|
+
|| expected.fileCount <= 0 || !Array.isArray(observations) || observations.length !== 3) {
|
|
47
|
+
return ['native production package execution evidence is incomplete'];
|
|
48
|
+
}
|
|
49
|
+
const failures = [];
|
|
50
|
+
for (const observed of observations) {
|
|
51
|
+
if (observed.name !== expected.name || observed.version !== expected.version
|
|
52
|
+
|| observed.treeSha256 !== expected.treeSha256 || observed.fileCount !== expected.fileCount
|
|
53
|
+
|| typeof observed.path !== 'string' || !observed.path
|
|
54
|
+
|| !Number.isFinite(Date.parse(observed.observedAt))) failures.push('native npx package identity differs');
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(proof.runs) || proof.runs.length !== 2) return [...failures, 'native package run evidence is missing'];
|
|
57
|
+
for (let i = 0; i < 2; i++) {
|
|
58
|
+
const run = proof.runs[i].receipt;
|
|
59
|
+
if (run?.desiredVersion !== expected.version
|
|
60
|
+
|| Date.parse(observations[i].observedAt) > Date.parse(run?.startedAt)
|
|
61
|
+
|| Date.parse(observations[i + 1].observedAt) < Date.parse(run?.finishedAt)
|
|
62
|
+
|| Date.parse(observations[i + 1].observedAt) > Date.parse(proof.observedAt)) {
|
|
63
|
+
failures.push('native package observation does not bound the exact-version run');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return failures;
|
|
67
|
+
}
|