ruvnet-brain 4.3.9 → 4.3.10

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 (83) hide show
  1. package/README.md +26 -9
  2. package/bin/install.mjs +646 -366
  3. package/bin/nightly-refresh.mjs +115 -0
  4. package/kb/brain-profile.mjs +118 -32
  5. package/kb/lifecycle-evidence-retention.mjs +237 -0
  6. package/kb/refresh-run.mjs +367 -0
  7. package/kb/retrieval-result.mjs +39 -0
  8. package/kb/update-storage-transaction.mjs +439 -0
  9. package/package.json +11 -2
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/hooks/codex-hooks.json +2 -165
  13. package/plugin/hooks/hook-contracts.json +4 -65
  14. package/plugin/hooks/hooks.json +2 -208
  15. package/plugin/scripts/adr-currency-gate.mjs +29 -19
  16. package/plugin/scripts/capability-registry.mjs +10 -83
  17. package/plugin/scripts/codex-hook-adapter.mjs +62 -9
  18. package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
  19. package/plugin/scripts/continuation-gate.mjs +30 -28
  20. package/plugin/scripts/continuation-objective.mjs +34 -0
  21. package/plugin/scripts/development-maintenance.mjs +49 -0
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/learn-flush.mjs +6 -1
  24. package/plugin/scripts/lesson-gate.mjs +5 -2
  25. package/plugin/scripts/lesson-presentation.mjs +5 -0
  26. package/plugin/scripts/md-stamp.mjs +109 -6
  27. package/plugin/scripts/memory-doctor.mjs +3 -7
  28. package/plugin/scripts/nightly-controller.mjs +9 -27
  29. package/plugin/scripts/nightly-scheduler.mjs +472 -0
  30. package/plugin/scripts/project-progression-contract.mjs +4 -4
  31. package/plugin/scripts/project-progression-store.mjs +35 -6
  32. package/plugin/scripts/ruflo-bin.mjs +20 -0
  33. package/plugin/scripts/session-snapshot-contract.mjs +6 -1
  34. package/plugin/scripts/version-bump-gate.sh +14 -2
  35. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
  36. package/scripts/build-bundle.mjs +8 -0
  37. package/scripts/build-primer.mjs +2 -3
  38. package/scripts/candidate-host-evidence.mjs +59 -41
  39. package/scripts/ci/build-fixture-kb.mjs +59 -2
  40. package/scripts/ci/mutate-hook-timeout.mjs +24 -47
  41. package/scripts/ci/stranger-scenario.mjs +13 -28
  42. package/scripts/claims-verify.mjs +219 -21
  43. package/scripts/console-engine.mjs +4 -18
  44. package/scripts/console-runtime-identity.mjs +6 -0
  45. package/scripts/development-maintenance.mjs +47 -0
  46. package/scripts/development-push-check.mjs +35 -0
  47. package/scripts/distill-project.mjs +24 -47
  48. package/scripts/doc-currency.mjs +91 -19
  49. package/scripts/git-hooks/pre-push +4 -162
  50. package/scripts/health-repair.mjs +22 -4
  51. package/scripts/hook-retirement-check.mjs +22 -0
  52. package/scripts/host-install-matrix.mjs +125 -61
  53. package/scripts/integration-evidence.mjs +11 -6
  54. package/scripts/learning-replay-fixture.mjs +23 -2
  55. package/scripts/nightly-two-run-proof.mjs +415 -0
  56. package/scripts/nightly-watchdog.mjs +17 -2
  57. package/scripts/npm-invocation.mjs +20 -0
  58. package/scripts/prepublication-evidence.mjs +42 -6
  59. package/scripts/primer-grounding.mjs +20 -0
  60. package/scripts/product-integrity-contract.mjs +22 -2
  61. package/scripts/public-verification-abandon.mjs +101 -0
  62. package/scripts/public-verification-aggregate.mjs +53 -19
  63. package/scripts/public-verification-finalizer.mjs +8 -2
  64. package/scripts/public-verification-lane.mjs +63 -8
  65. package/scripts/publication-receipt.mjs +109 -45
  66. package/scripts/published-surface-probe.mjs +10 -2
  67. package/scripts/qa-contract.mjs +57 -0
  68. package/scripts/qa-lanes.mjs +40 -0
  69. package/scripts/qa-runner.mjs +54 -64
  70. package/scripts/qe/ux-suite.mjs +10 -34
  71. package/scripts/qualified-candidate-check.mjs +124 -0
  72. package/scripts/release-abort-stale.mjs +2 -2
  73. package/scripts/release-projection.mjs +20 -1
  74. package/scripts/release-qualification-contract.mjs +87 -0
  75. package/scripts/release-qualification.mjs +135 -0
  76. package/scripts/release-transaction.mjs +81 -1
  77. package/scripts/remedy-registry.mjs +6 -13
  78. package/scripts/retrieval-canary.mjs +91 -18
  79. package/scripts/selfcheck.mjs +12 -4
  80. package/scripts/snapshot-freshness.mjs +58 -0
  81. package/scripts/stack-sync.mjs +41 -43
  82. package/scripts/staged-host-verifier.mjs +63 -14
  83. 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
- 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 };
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 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 }));
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 receipt = buildIntegrationEvidence(JSON.parse(fs.readFileSync(value('--report'), 'utf8')), {
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
  }