ruvnet-brain 4.3.27 → 4.3.29

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 (78) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +74 -7
  3. package/console/app.js +7 -2
  4. package/kb/corpus-release-identity.mjs +73 -0
  5. package/package.json +4 -3
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/mcp/managed-cli-interface.mjs +83 -9
  9. package/plugin/scripts/advocacy-route.mjs +4 -1
  10. package/plugin/scripts/capacity-aware-parallel-work.mjs +4 -0
  11. package/plugin/scripts/continuation-gate.mjs +71 -0
  12. package/plugin/scripts/decision-gate.mjs +29 -57
  13. package/plugin/scripts/ground-ruvnet.sh +77 -4
  14. package/plugin/scripts/grounding-stamp.sh +34 -5
  15. package/plugin/scripts/grounding-turn-gate.mjs +8 -2
  16. package/plugin/scripts/grounding-turn-mark.mjs +6 -1
  17. package/plugin/scripts/hook-input.mjs +54 -0
  18. package/plugin/scripts/hook-shim.mjs +10 -0
  19. package/plugin/scripts/memory-doctor.mjs +43 -0
  20. package/plugin/scripts/nightly-controller.mjs +6 -1
  21. package/plugin/scripts/project-progression-contract.mjs +17 -0
  22. package/plugin/scripts/project-progression-hook.mjs +18 -5
  23. package/plugin/scripts/project-progression-producer.mjs +16 -12
  24. package/plugin/scripts/ruvnet-gate1-pattern.mjs +17 -0
  25. package/plugin/scripts/session-start-core.mjs +11 -5
  26. package/plugin/scripts/session-start-update-plane.mjs +5 -1
  27. package/plugin/scripts/unprompted-runtime.mjs +29 -8
  28. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +1 -1
  29. package/scripts/adr-072-completion.mjs +2 -1
  30. package/scripts/calibrate-router.mjs +6 -6
  31. package/scripts/candidate-host-evidence.mjs +44 -16
  32. package/scripts/corpus-reconcile.mjs +32 -3
  33. package/scripts/correction-detect.mjs +10 -11
  34. package/scripts/dispatch-receipt.mjs +2 -2
  35. package/scripts/dual-host-deliberation.mjs +3 -2
  36. package/scripts/execution-policy.mjs +10 -2
  37. package/scripts/gen-console-images.mjs +0 -1
  38. package/scripts/gen-images.mjs +0 -4
  39. package/scripts/host-install-matrix.mjs +60 -14
  40. package/scripts/independent-review-receipt.mjs +7 -8
  41. package/scripts/ingest-repo.mjs +45 -1
  42. package/scripts/learning-replay-cli.mjs +2 -1
  43. package/scripts/learning-replay-fixture.mjs +2 -1
  44. package/scripts/learnings.mjs +19 -5
  45. package/scripts/lesson-migrate-agentdb.mjs +635 -0
  46. package/scripts/loop-checkpoint.mjs +37 -1
  47. package/scripts/metaharness-receipts.mjs +3 -2
  48. package/scripts/nightly-two-run-proof.mjs +6 -6
  49. package/scripts/nightly-watchdog.mjs +10 -2
  50. package/scripts/onboarding-console.mjs +98 -49
  51. package/scripts/oracle/producer-hosts.mjs +2 -1
  52. package/scripts/prepublication-evidence.mjs +8 -0
  53. package/scripts/private-overlay.mjs +31 -9
  54. package/scripts/public-verification-aggregate.mjs +4 -3
  55. package/scripts/publication-receipt.mjs +80 -30
  56. package/scripts/qe/agentic-qe-4.3.mjs +0 -1
  57. package/scripts/qe/ux-suite.mjs +17 -5
  58. package/scripts/rebuild-gists-from-receipts.mjs +1 -1
  59. package/scripts/reconcile-project.mjs +0 -0
  60. package/scripts/release-qualification-contract.mjs +11 -1
  61. package/scripts/release.mjs +34 -82
  62. package/scripts/retrieval-canary.mjs +3 -2
  63. package/scripts/review-model-defaults.mjs +26 -0
  64. package/scripts/route-cheap.mjs +20 -7
  65. package/scripts/router-utilization.mjs +5 -5
  66. package/scripts/rvf-generation.mjs +68 -2
  67. package/scripts/single-source-check.mjs +270 -0
  68. package/scripts/staged-host-verifier.mjs +2 -1
  69. package/scripts/subscription-hosts.mjs +4 -0
  70. package/scripts/sync-version.mjs +22 -0
  71. package/scripts/trismart.mjs +3 -3
  72. package/scripts/wired-check.mjs +40 -4
  73. package/console/assets/memory.webp +0 -0
  74. package/plugin/scripts/version-bump-gate.sh +0 -124
  75. package/scripts/qe/aggregate-4.3.mjs +0 -42
  76. package/scripts/release-convergence-watchdog.mjs +0 -112
  77. package/scripts/stamp-existing-rvf-generations.mjs +0 -53
  78. package/scripts/verify-channels.mjs +0 -196
@@ -32,6 +32,14 @@ import { runRetrievalCanaries, validateRetrievalCanaryReceipt, resolveInstalledC
32
32
 
33
33
  /** The three host shapes a release must survive. ONE name each, for every consumer. */
34
34
  export const HOST_MODES = Object.freeze(['claude', 'codex', 'dual']);
35
+ export const SELF_STORE_PROOF_QUERY = 'repo:ruvnet-brain What is the RuvNet Brain release evidence workflow?';
36
+ export const SELF_STORE_PROOF_K = 1;
37
+ export const RELEASE_SEARCH_QUERY = 'repo:ruvnet-brain How does RuvNet Brain prove a public release artifact?';
38
+ export const HOST_WARMUP_TIMEOUT_MS = 300_000;
39
+ export const RELEASE_SEARCH_DEADLINE_MS = 30_000;
40
+ // The shared model-cache prewarm's own candidate pool size, named so the test asserting on it
41
+ // derives from this constant instead of restating the digit as a second, driftable literal.
42
+ export const PREWARM_POOL_SIZE = 8;
35
43
 
36
44
  /** Which CLIs each mode is allowed to see. A codex-only box genuinely has no `claude`. */
37
45
  export const MODE_HOSTS = Object.freeze({
@@ -204,6 +212,7 @@ export async function runHostMatrixAsync({
204
212
  verifyGrounding = verifyInstalledGrounding,
205
213
  resolveMcpServer = resolveInstalledMcpServer,
206
214
  retrieval,
215
+ sequentialSearches = false,
207
216
  }) {
208
217
  const spec = VARIANTS[variant];
209
218
  if (!spec) throw new Error(`unknown host-matrix variant: ${variant}`);
@@ -251,54 +260,91 @@ export async function runHostMatrixAsync({
251
260
 
252
261
  const prewarmContext = contexts[0];
253
262
  const prewarmReader = path.join(prewarmContext.env.RUVNET_BRAIN_KB, 'forge-ask-all.mjs');
263
+ // This query only primes the shared model cache. Keep it in the Brain's own bounded store so
264
+ // Mac/Windows/Linux runner speed does not determine whether the later real host searches run.
265
+ // The following MCP matrix remains the measured, source-grounded candidate acceptance.
254
266
  const prewarm = await runCommand(process.execPath, [prewarmReader, '--dir', prewarmContext.env.RUVNET_BRAIN_KB,
255
- '--q', 'How does RuvNet Brain prove a public release artifact?', '--k', '1'], {
267
+ '--q', RELEASE_SEARCH_QUERY, '--k', '1', '--pool', String(PREWARM_POOL_SIZE),
268
+ '--repos', 'ruvnet-brain', '--bounded'], {
256
269
  cwd: prewarmContext.env.RUVNET_BRAIN_KB, env: prewarmContext.env, timeout: 300_000,
257
270
  });
258
271
  if (prewarm.error || prewarm.status !== 0) {
259
- return { verdict: 'FAIL', fixtures: {}, error: `shared model prewarm failed (${processDiagnostic(prewarm)})` };
272
+ const detail = String(prewarm.stderr || prewarm.stdout || '').trim().split('\n').slice(-8).join(' | ').slice(-1600);
273
+ return { verdict: 'FAIL', fixtures: {}, error: `shared model prewarm failed (${processDiagnostic(prewarm)})${detail ? `; ${detail}` : ''}` };
260
274
  }
261
275
  const prewarmGrounding = await verifyGrounding(String(prewarm.stdout || ''), prewarmContext.env.RUVNET_BRAIN_KB);
262
276
  if (!prewarmGrounding?.grounded) {
263
277
  return { verdict: 'FAIL', fixtures: {}, error: 'shared model prewarm returned no grounded source receipt' };
264
278
  }
265
279
 
266
- const searches = await Promise.all(contexts.map(async (context) => {
280
+ const searchOne = async (context) => {
267
281
  let session;
268
282
  try {
269
283
  const serverPath = resolveMcpServer(context);
270
284
  // One installed worker per host: model/store state survives smoke and every sealed case.
271
285
  session = runMcpSearch === runInstalledMcpSearch ? createInstalledMcpSession({ serverPath, env: context.env }) : null;
272
286
  const searchMcp = session ? (args) => session.search(args) : runMcpSearch;
273
- const processResult = await searchMcp({ mode: context.mode, serverPath, env: context.env });
287
+ // The installed shell warms its in-process models and stores asynchronously from MCP
288
+ // initialize. Candidate qualification must give that same worker the same cited readiness
289
+ // probe used by the public verifier before timing the first user search; a separate CLI
290
+ // prewarm only primes disk cache and cannot warm this worker's process memory.
291
+ const warmupResult = await searchMcp({ mode: context.mode, serverPath, env: context.env,
292
+ query: SELF_STORE_PROOF_QUERY, k: SELF_STORE_PROOF_K, timeoutMs: HOST_WARMUP_TIMEOUT_MS });
293
+ const warmupOutput = `${warmupResult.stdout || ''}${warmupResult.stderr || ''}`;
294
+ if (warmupResult.error || warmupResult.status !== 0) {
295
+ return { context, processResult: warmupResult,
296
+ error: `MCP warmup failed for ${context.mode} (${processDiagnostic(warmupResult)}): ${warmupOutput.slice(-2000)}` };
297
+ }
298
+ const warmupGrounding = await verifyGrounding(warmupOutput, context.env.RUVNET_BRAIN_KB);
299
+ if (!warmupGrounding?.grounded || warmupGrounding.receipt?.repo !== 'ruvnet-brain') {
300
+ return { context, processResult: warmupResult, warmupMs: warmupResult.broadMs,
301
+ error: `MCP self-store warmup grounding unproven for ${context.mode}` };
302
+ }
303
+ const processResult = await searchMcp({ mode: context.mode, serverPath, env: context.env,
304
+ query: RELEASE_SEARCH_QUERY, k: 5,
305
+ timeoutMs: RELEASE_SEARCH_DEADLINE_MS });
274
306
  const output = `${processResult.stdout || ''}${processResult.stderr || ''}`;
275
307
  if (processResult.error || processResult.status !== 0) {
276
- return { context, processResult, error: `MCP search failed for ${context.mode} (${processDiagnostic(processResult)}): ${output.slice(-2000)}` };
308
+ return { context, processResult, warmupMs: warmupResult.broadMs, warmupGrounding,
309
+ error: `MCP search failed for ${context.mode} (${processDiagnostic(processResult)}): ${output.slice(-2000)}` };
277
310
  }
278
311
  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}` };
312
+ if (!grounding?.grounded) return { context, processResult, warmupMs: warmupResult.broadMs,
313
+ warmupGrounding, error: `MCP search grounding unproven for ${context.mode}` };
280
314
  let receipt;
281
315
  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 });
316
+ receipt = await runRetrievalCanaries({ ...retrieval, searchTimeoutMs: RELEASE_SEARCH_DEADLINE_MS,
317
+ search: async ({ query, k, timeoutMs }) => {
318
+ const result = await searchMcp({ mode: context.mode, serverPath, env: context.env, query, k, timeoutMs });
285
319
  if (result.error || result.status !== 0) throw new Error(`canary MCP search failed: ${processDiagnostic(result)}`);
286
320
  return parseRetrievalResult(result.mcpResult, { query, k });
287
321
  },
288
322
  citationResolver: (matched, expected) => resolveInstalledCanaryCitation({ kbDir: context.env.RUVNET_BRAIN_KB, matched, expected }),
289
323
  });
290
324
  try { validateRetrievalCanaryReceipt(receipt, { plan: retrieval.plan }); }
291
- catch (error) { return { context, processResult, grounding, retrieval: receipt, error: `${context.mode} canary rejected: ${error.message}` }; }
325
+ catch (error) { return { context, processResult, warmupMs: warmupResult.broadMs,
326
+ warmupGrounding, grounding, retrieval: receipt, error: `${context.mode} canary rejected: ${error.message}` }; }
292
327
  }
293
- return { context, processResult, grounding, ...(receipt ? { retrieval: receipt } : {}) };
328
+ return { context, processResult, warmupMs: warmupResult.broadMs, warmupGrounding,
329
+ grounding, ...(receipt ? { retrieval: receipt } : {}) };
294
330
  } catch (error) {
295
331
  return { context, processResult: { status: null, error }, error: `${context.mode} host search failed: ${error.message}` };
296
332
  } finally { await session?.close(); }
297
- }));
298
- const fixtures = Object.fromEntries(searches.map(({ context, processResult, grounding, retrieval, error }) => [context.mode, {
333
+ };
334
+ const searches = [];
335
+ if (sequentialSearches) {
336
+ for (const context of contexts) searches.push(await searchOne(context));
337
+ } else {
338
+ searches.push(...await Promise.all(contexts.map(searchOne)));
339
+ }
340
+ const fixtures = Object.fromEntries(searches.map(({ context, processResult, warmupMs, warmupGrounding,
341
+ grounding, retrieval, error }) => [context.mode, {
299
342
  status: error ? 'FAIL' : 'PASS',
300
343
  version,
301
344
  process: processIdentity(processResult),
345
+ warmupMs: Number.isFinite(warmupMs) ? warmupMs : null,
346
+ ...(warmupGrounding?.receipt ? { warmupGrounding: warmupGrounding.receipt } : {}),
347
+ searchMs: Number.isFinite(processResult.broadMs) ? processResult.broadMs : null,
302
348
  ...(grounding?.receipt ? { grounding: grounding.receipt } : {}),
303
349
  ...(retrieval ? { retrieval } : {}),
304
350
  ...(error ? { error } : {}),
@@ -395,7 +441,7 @@ export function createInstalledMcpSession({ serverPath, env, timeout = 300_000,
395
441
  pending.set(id, { resolve, reject });
396
442
  child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
397
443
  });
398
- const execute = async ({ query = 'How does RuvNet Brain prove a public release artifact?', k = 5,
444
+ const execute = async ({ query = RELEASE_SEARCH_QUERY, k = 5,
399
445
  timeoutMs = timeout } = {}) => {
400
446
  if (terminalError) return { status: null, error: terminalError, signal: exitSignal, stdout: '', stderr };
401
447
  stderr = '';
@@ -5,12 +5,11 @@ import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { canonicalJson, digest } from './coverage-integrity.mjs';
7
7
  import { transactionIdFor } from './release-transaction.mjs';
8
+ import { CLAUDE_FABLE_5_ID, GPT_5_6_SOL_ID, LEGACY_REVIEWER_IDENTITIES } from './review-model-defaults.mjs';
8
9
  const HEX40 = /^[a-f0-9]{40}$/;
9
10
  const HEX64 = /^[a-f0-9]{64}$/;
10
11
  const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
11
- export const ALLOWED_INDEPENDENT_REVIEWERS = Object.freeze([
12
- Object.freeze({ identity: 'claude-fable-5', model: 'claude-fable-5', provider: 'firstParty' }),
13
- Object.freeze({ identity: 'gpt-5.6-sol', model: 'gpt-5.6-sol', provider: 'openai' })]);
12
+ export const ALLOWED_INDEPENDENT_REVIEWERS = LEGACY_REVIEWER_IDENTITIES;
14
13
 
15
14
  const INPUT_KEYS = Object.freeze([
16
15
  'artifactSha256', 'deductions', 'execution', 'findings', 'id', 'independent', 'model', 'payloadId',
@@ -222,17 +221,17 @@ function reviewerFor(input) {
222
221
  }
223
222
 
224
223
  function normalizeExecution(execution, reviewer) {
225
- const required = reviewer.identity === 'gpt-5.6-sol'
224
+ const required = reviewer.identity === GPT_5_6_SOL_ID
226
225
  ? ['catalogRowSha256', 'invocationDigest', 'subscriptionAuthenticated', 'threadId']
227
226
  : ['invocationDigest', 'subscriptionAuthenticated'];
228
- if (reviewer.identity === 'gpt-5.6-sol'
227
+ if (reviewer.identity === GPT_5_6_SOL_ID
229
228
  && (!Object.hasOwn(execution || {}, 'threadId') || !Object.hasOwn(execution || {}, 'catalogRowSha256'))) {
230
229
  throw new Error('GPT review thread and catalog evidence are required');
231
230
  }
232
231
  exactKeys(execution, required, 'review execution');
233
232
  if (execution.subscriptionAuthenticated !== true) throw new Error('review execution is not subscription authenticated');
234
233
  hex(execution.invocationDigest, HEX64, 'review invocation digest');
235
- if (reviewer.identity === 'gpt-5.6-sol') {
234
+ if (reviewer.identity === GPT_5_6_SOL_ID) {
236
235
  text(execution.threadId, 'GPT review thread');
237
236
  hex(execution.catalogRowSha256, HEX64, 'GPT review catalog row');
238
237
  }
@@ -479,8 +478,8 @@ export function main(args = process.argv.slice(2), runtime = {}) {
479
478
  readJson(options['--sol'], 'Sol review receipt')];
480
479
  const ordered = validateIndependentReviewPair(receipts, {
481
480
  publicKeysByReviewer: {
482
- 'claude-fable-5': readRegular(options['--fable-public-key'], 'Fable review public key'),
483
- 'gpt-5.6-sol': readRegular(options['--sol-public-key'], 'Sol review public key'),
481
+ [CLAUDE_FABLE_5_ID]: readRegular(options['--fable-public-key'], 'Fable review public key'),
482
+ [GPT_5_6_SOL_ID]: readRegular(options['--sol-public-key'], 'Sol review public key'),
484
483
  },
485
484
  expectedIdentity: options['--expected-identity']
486
485
  ? readJson(options['--expected-identity'], 'expected review identity') : null,
@@ -21,6 +21,7 @@ import { execFileSync } from 'node:child_process';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { FULL_HINTS, KEEP_DIRS } from './full-hints.mjs';
23
23
  import { storeRoot } from '../kb/store-root.mjs';
24
+ import { applyPrivateOverlay } from './private-overlay.mjs';
24
25
 
25
26
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
27
  const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
@@ -118,15 +119,58 @@ const carded = (() => {
118
119
  // The stores themselves cannot be committed. The FACT that they were ingested can, so a wipe
119
120
  // becomes detectable and replayable instead of silent and permanent. This record is the recipe.
120
121
  if (ok) {
122
+ let rest = [];
121
123
  try {
122
124
  const ledger = path.join(ROOT, 'kb', 'local-ingests.json');
123
125
  const prior = fs.existsSync(ledger) ? JSON.parse(fs.readFileSync(ledger, 'utf8')) : { ingests: [] };
124
- const rest = (prior.ingests || []).filter((e) => e.name !== NAME);
126
+ rest = (prior.ingests || []).filter((e) => e.name !== NAME);
125
127
  rest.push({ name: NAME, org: ORG, at: new Date().toISOString(), store: kb });
126
128
  rest.sort((a, b) => a.name.localeCompare(b.name));
127
129
  fs.writeFileSync(ledger, `${JSON.stringify({ ingests: rest }, null, 2)}\n`);
128
130
  console.log(`[record] kb/local-ingests.json — ${rest.length} local ingest(s) recorded. COMMIT THIS.`);
129
131
  } catch (e) { console.log(`[record] could not record the ingest: ${e.message}`); }
132
+
133
+ // S3 (explicit local ownership): stamp updateManaged:false + origin:'local-ingest' into the LIVE
134
+ // store root's own SOURCE.json (via scripts/private-overlay.mjs's writer — --from equals --root
135
+ // here, since forge-refresh.mjs already wrote the bytes directly into KB; the writer degenerates
136
+ // to registry-stamping only, see its own header). Without this a store built through THIS script
137
+ // reads exactly like a normal public store to kb/forge-update.mjs — unmarked, and therefore
138
+ // silently deleted the moment it is absent from an incoming public bundle.
139
+ //
140
+ // ONE-TIME MIGRATION for stores this script ingested BEFORE this fix shipped: every OTHER name
141
+ // this ledger already recorded is retroactively re-stamped too, provided its files still exist and
142
+ // it is not already an update-managed (public) SOURCE.json entry. This runs HERE — where the
143
+ // recipe ledger (kb/local-ingests.json, git-tracked in the checkout) and the live store root (KB,
144
+ // ~/.cache/ruvnet-brain/kb by default — a DIFFERENT, non-git-tracked directory, see kb/store-
145
+ // root.mjs) are both actually in scope in the same process — rather than inside kb/forge-update.mjs,
146
+ // which runs against an installed KB tree with no reliable path back to this checkout's ledger.
147
+ const candidateNames = [...new Set([kb, ...rest.map((e) => e.store).filter(Boolean)])];
148
+ const sourceFile = path.join(KB, 'SOURCE.json');
149
+ let liveSourceStores = {};
150
+ try { liveSourceStores = JSON.parse(fs.readFileSync(sourceFile, 'utf8')).stores || {}; } catch { /* handled by applyPrivateOverlay below */ }
151
+ const toStamp = candidateNames.filter((name) => {
152
+ const existing = liveSourceStores[name];
153
+ if (existing && existing.updateManaged !== false) return false; // a real public store — never shadow it
154
+ return [`${name}.big.rvf`, `${name}.big.rvf.embed.json`, `${name}.big.rvf.idmap.json`, `${name}.meta.json`, `${name}.passages.jsonl`]
155
+ .every((file) => fs.existsSync(path.join(KB, file)));
156
+ });
157
+ if (toStamp.length) {
158
+ try {
159
+ const fenceFile = path.join(KB, 'PRIVATE-STORES.json');
160
+ const fence = fs.existsSync(fenceFile) ? JSON.parse(fs.readFileSync(fenceFile, 'utf8')) : { privateStores: [] };
161
+ const fenceNames = new Set((fence.privateStores || []).map((n) => String(n).toLowerCase()));
162
+ const additions = toStamp.filter((name) => !fenceNames.has(name.toLowerCase()));
163
+ if (additions.length) {
164
+ fence.privateStores = [...new Set([...(fence.privateStores || []), ...additions])].sort();
165
+ fs.writeFileSync(fenceFile, `${JSON.stringify(fence, null, 2)}\n`);
166
+ }
167
+ const receipt = applyPrivateOverlay({ root: KB, from: KB, stores: toStamp, origin: 'local-ingest' });
168
+ const stamped = receipt.stores.filter((entry) => entry.changes.includes('SOURCE.json')).map((entry) => entry.name);
169
+ if (stamped.length) console.log(`[local-ownership] marked updateManaged:false (origin: local-ingest) — ${stamped.join(', ')}`);
170
+ } catch (e) {
171
+ console.log(`[local-ownership] could not stamp local ownership (${e.message}); the next forge-update.mjs --apply may remove ${toStamp.join(', ')} if they are absent from the incoming bundle`);
172
+ }
173
+ }
130
174
  }
131
175
 
132
176
  console.log(!ok
@@ -35,6 +35,7 @@ import {
35
35
  writeArtifact,
36
36
  } from './learning-replay-proof.mjs';
37
37
  import { ROOT } from './learning-replay-contract.mjs';
38
+ import { GPT_5_6_SOL_ID } from './review-model-defaults.mjs';
38
39
 
39
40
  const usage = () => `Usage:
40
41
  node scripts/learning-replay.mjs [--trap ${TRAP.MEMORY_SEARCH}|${TRAP.POST_TASK}] [--n N] [--host codex|claude-code] [--model MODEL]
@@ -94,7 +95,7 @@ export async function main(argv = process.argv.slice(2)) {
94
95
  const mutant = arg('--mutant', null);
95
96
  const trap = arg('--trap', TRAP.MEMORY_SEARCH);
96
97
  const host = arg('--host', 'codex');
97
- const model = arg('--model', host === 'codex' ? 'gpt-5.6-sol' : 'haiku');
98
+ const model = arg('--model', host === 'codex' ? GPT_5_6_SOL_ID : 'haiku');
98
99
  const n = Math.max(1, parseInt(arg('--n', mutant ? '1' : '3'), 10) || 1);
99
100
  const outFile = arg('--out', mutant
100
101
  ? MUTANT_RESULT_FILES[trap]?.[mutant]
@@ -19,6 +19,7 @@ import {
19
19
  PROJECT_B_MEMORY_VALUE,
20
20
  RUFLO_BIN,
21
21
  } from './learning-replay-execution.mjs';
22
+ import { GPT_5_6_SOL_ID } from './review-model-defaults.mjs';
22
23
 
23
24
  const CLAUDE_BIN = process.env.RUVNET_CLAUDE_BIN
24
25
  || path.join(os.homedir(), '.npm-global', 'bin', 'claude');
@@ -266,7 +267,7 @@ export function codexReplayInstrumentationError(events, sequence, attempts) {
266
267
  }
267
268
 
268
269
  export function buildCodexArgv({
269
- model = 'gpt-5.6-sol',
270
+ model = GPT_5_6_SOL_ID,
270
271
  prompt = REPLAY_PROMPT,
271
272
  appendSystemPrompt = null,
272
273
  brainHome,
@@ -8,13 +8,22 @@ import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { learningScope, learnerCwd } from '../plugin/scripts/runtime-preferences.mjs';
11
12
 
12
13
  const HOME = os.homedir();
13
14
 
14
- /** @param {{statsPath?:string, queueDir?:string, now?:number}} [opts] */
15
- export function learnings({ statsPath, queueDir, now = Date.now() } = {}) {
16
- const sp = statsPath || path.join(HOME, '.claude-flow/neural/stats.json');
17
- const qd = queueDir || path.join(HOME, '.cache/ruvnet-brain/learn');
15
+ /** @param {{statsPath?:string, queueDir?:string, now?:number, cwd?:string, env?:object, home?:string}} [opts] */
16
+ export function learnings({ statsPath, queueDir, now = Date.now(), cwd, env, home } = {}) {
17
+ // The flusher and the console must inspect the same learner. A fixed HOME path made the panel
18
+ // report a stale global learner while project-scoped captures were fed under the active project.
19
+ // Keep injectable paths for callers/tests, but derive both defaults from the shared scope owner.
20
+ const project = cwd || process.env.RUVNET_BRAIN_PROJECT_DIR || process.cwd();
21
+ const scope = learningScope({ cwd: project, env: env || process.env });
22
+ const learner = learnerCwd({ cwd: project, env: env || process.env, home: home || HOME });
23
+ const sp = statsPath || path.join(learner, '.claude-flow', 'neural', 'stats.json');
24
+ const qd = queueDir || (scope === 'user'
25
+ ? path.join(home || HOME, '.cache', 'ruvnet-brain', 'learn')
26
+ : path.join(project, '.swarm', 'ruvnet-brain-learn'));
18
27
 
19
28
  let stats = {};
20
29
  try { stats = JSON.parse(fs.readFileSync(sp, 'utf8')); } catch { /* no learner yet */ }
@@ -53,7 +62,12 @@ export function learnings({ statsPath, queueDir, now = Date.now() } = {}) {
53
62
  lastAdaptation: lastMs ? new Date(lastMs).toISOString() : null,
54
63
  daysSinceLastAdaptation: daysSince,
55
64
  recentWorkflow,
56
- note: 'Learnings are how you work — shared across all your projects and getting smarter over time. Project facts stay isolated per project; nothing here is project data.',
65
+ note: scope === 'user'
66
+ ? 'Learnings are how you work — shared across your projects and getting smarter over time. Project facts stay isolated per project; nothing here is project data.'
67
+ : 'Learnings are how you work in this project and getting smarter over time. User-scoped learning remains isolated from this project; nothing here is project data.',
68
+ scope,
69
+ statsPath: sp,
70
+ queueDir: qd,
57
71
  };
58
72
  }
59
73