ruvnet-brain 4.3.8 → 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 (85) 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 +108 -8
  65. package/scripts/publication-receipt.mjs +173 -66
  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-publication-handoff.mjs +101 -0
  75. package/scripts/release-qualification-contract.mjs +87 -0
  76. package/scripts/release-qualification.mjs +135 -0
  77. package/scripts/release-transaction.mjs +81 -1
  78. package/scripts/release.mjs +10 -1
  79. package/scripts/remedy-registry.mjs +6 -13
  80. package/scripts/retrieval-canary.mjs +91 -18
  81. package/scripts/selfcheck.mjs +12 -4
  82. package/scripts/snapshot-freshness.mjs +58 -0
  83. package/scripts/stack-sync.mjs +41 -43
  84. package/scripts/staged-host-verifier.mjs +63 -14
  85. package/scripts/wired-check.mjs +31 -2
@@ -7,7 +7,6 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { pipeline } from 'node:stream/promises';
9
9
  import { Readable } from 'node:stream';
10
- import readline from 'node:readline';
11
10
  import { spawn, spawnSync } from 'node:child_process';
12
11
  // ONE doctor rule and ONE mode vocabulary, shared with the staged-side check in
13
12
  // scripts/staged-host-verifier.mjs. This file kept its own copies, spelled claudeOnly/
@@ -16,11 +15,12 @@ import { spawn, spawnSync } from 'node:child_process';
16
15
  // post-publication proofs below (payload assertions, MCP wiring, SOURCE.json, rpcSearch)
17
16
  // stay here — they are this side's job, not duplication.
18
17
  import { HOST_MODES, RECEIPT_MODE_NAMES, MODE_FROM_RECEIPT_NAME, classifyDoctor, VARIANTS } from './host-install-matrix.mjs';
19
- import { pathToFileURL } from 'node:url';
18
+ import { fileURLToPath, pathToFileURL } from 'node:url';
20
19
  import { evaluateCandidateReceipt, evaluatePublicationReceipt } from './release-proof.mjs';
21
20
  import { verifyPayload } from './release-payload.mjs';
22
- import { digest, sha256File } from './coverage-integrity.mjs';
23
- import { parseCitations } from '../kb/verify-citation.mjs';
21
+ import { resolveInstalledCanaryCitation } from './retrieval-canary.mjs';
22
+ import { runNightlyTwoRunProof, validateNightlyProofReceipt } from './nightly-two-run-proof.mjs';
23
+ import { parseRetrievalResult } from '../kb/retrieval-result.mjs';
24
24
 
25
25
  const REPO = 'stuinfla/ruvnet-brain';
26
26
  const PACKAGE = 'ruvnet-brain';
@@ -29,30 +29,95 @@ const DEADLINE_MS = 30_000;
29
29
  const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
30
30
  const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
31
31
  const receiptDigest = (candidate) => String(candidate?.artifact?.sha256 || '').replace(/^sha256:/, '');
32
-
32
+ export function commandInvocation(name, args, { platform = process.platform, env = process.env } = {}) {
33
+ if (platform === 'win32' && /^(?:npm|npx)$/i.test(name)) return {
34
+ executable: env.ComSpec || 'cmd.exe', args: ['/d', '/c', `${name}.cmd`, ...args],
35
+ };
36
+ return { executable: name, args };
37
+ }
38
+ export function tarExtractionInvocation(artifactPath, destination, { platform = process.platform } = {}) {
39
+ const paths = platform === 'win32' ? path.win32 : path;
40
+ const archive = paths.resolve(artifactPath);
41
+ // A relative archive argument avoids GNU tar's drive-letter remote-host syntax and also
42
+ // works with Windows BSD tar, which does not support GNU's --force-local option.
43
+ return { args: ['-xzf', `./${paths.basename(archive)}`, '-C', paths.resolve(destination)],
44
+ cwd: paths.dirname(archive) };
45
+ }
33
46
  function command(name, args, options = {}) {
34
- const result = spawnSync(name, args, { encoding: 'utf8', ...options });
47
+ const { platform = process.platform, ...spawnOptions } = options;
48
+ const invocation = commandInvocation(name, args, { platform, env: spawnOptions.env || process.env });
49
+ const result = spawnSync(invocation.executable, invocation.args, { encoding: 'utf8', ...spawnOptions });
35
50
  if (result.error || result.status !== 0) {
36
51
  const detail = String(result.stderr || result.stdout || result.error?.message || '').trim();
37
52
  throw new Error(`${name} ${args.join(' ')} failed: ${detail || `exit ${result.status}`}`);
38
53
  }
39
54
  return String(result.stdout || '').trim();
40
55
  }
41
-
42
- function locate(name) {
43
- try { return command('which', [name]); } catch { return null; }
56
+ export function validateCandidateSource(root, { sha, version }) {
57
+ const candidateRoot = fs.realpathSync(root);
58
+ const options = { cwd: candidateRoot };
59
+ const top = command('git', ['rev-parse', '--show-toplevel'], options);
60
+ const head = command('git', ['rev-parse', 'HEAD'], options);
61
+ // Git and Node can spell the same Windows directory with different casing/separators.
62
+ // Compare actual directory identity, never case-fold distinct source paths.
63
+ const candidateDirectory = fs.statSync(candidateRoot, { bigint: true });
64
+ const topDirectory = fs.statSync(top, { bigint: true });
65
+ if (!candidateDirectory.isDirectory() || !topDirectory.isDirectory()
66
+ || candidateDirectory.ino <= 0n || topDirectory.ino <= 0n
67
+ || candidateDirectory.dev !== topDirectory.dev || candidateDirectory.ino !== topDirectory.ino
68
+ || !/^[a-f0-9]{40}$/.test(String(sha)) || head !== sha) {
69
+ throw new Error(`candidate checkout ${head} does not match candidate ${sha}`);
70
+ }
71
+ if (command('git', ['status', '--porcelain', '--untracked-files=no'], options)) {
72
+ throw new Error('candidate checkout has tracked changes');
73
+ }
74
+ const manifest = path.join(candidateRoot, 'package.json');
75
+ const stat = fs.lstatSync(manifest);
76
+ if (!stat.isFile() || stat.isSymbolicLink() || !version || readJson(manifest).version !== version) {
77
+ throw new Error('candidate checkout package version differs from the released candidate');
78
+ }
79
+ return candidateRoot;
44
80
  }
45
-
46
- function isolatedPath(mode, temp) {
81
+ function locate(name, { platform = process.platform } = {}) {
82
+ const query = platform === 'win32' ? (name === 'node' ? 'node.exe' : `${name}.cmd`) : name;
83
+ try {
84
+ return command(platform === 'win32' ? 'where.exe' : 'which', [query], { platform })
85
+ .split(/\r?\n/).find(Boolean) || null;
86
+ } catch { return null; }
87
+ }
88
+ export function createIsolatedPath(mode, temp, { platform = process.platform, env = process.env,
89
+ resolve = (name) => locate(name, { platform }) } = {}) {
47
90
  const bin = path.join(temp, `bin-${mode}`);
48
91
  fs.mkdirSync(bin);
49
92
  const hosts = mode === 'claudeOnly' ? ['claude'] : mode === 'codexOnly' ? ['codex'] : ['claude', 'codex'];
50
- for (const name of ['node', 'npm', ...hosts]) {
51
- const target = locate(name);
93
+ const names = platform === 'win32' ? ['npm', ...hosts] : ['node', 'npm', ...hosts];
94
+ for (const name of names) {
95
+ const target = resolve(name);
52
96
  if (!target) throw new Error(`${name} CLI unavailable for ${mode} public host fixture`);
53
- fs.symlinkSync(target, path.join(bin, name));
97
+ if (platform === 'win32') {
98
+ fs.writeFileSync(path.join(bin, `${name}.cmd`), `@call "${target}" %*\r\n`, { flag: 'wx' });
99
+ } else {
100
+ fs.symlinkSync(target, path.join(bin, name));
101
+ }
54
102
  }
55
- return `${bin}:/usr/bin:/bin`;
103
+ if (platform !== 'win32') return `${bin}:/usr/bin:/bin`;
104
+ const node = resolve('node');
105
+ if (!node) throw new Error(`node CLI unavailable for ${mode} public host fixture`);
106
+ return [bin, path.dirname(node), path.join(env.SystemRoot || 'C:\\Windows', 'System32'),
107
+ path.join(env.ProgramFiles || 'C:\\Program Files', 'Git', 'usr', 'bin')].join(path.win32.delimiter);
108
+ }
109
+ export function stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot }) {
110
+ const source = path.resolve(bundlePath || '');
111
+ let stat;
112
+ try { stat = fs.lstatSync(source); } catch { throw new Error('verified public bundle is missing'); }
113
+ if (!stat.isFile() || stat.isSymbolicLink() || sha256(source) !== bundleSha256) {
114
+ throw new Error('verified bundle digest mismatch before installation');
115
+ }
116
+ const destination = path.join(packageRoot, 'dist', 'ruvnet-brain.zip');
117
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
118
+ fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
119
+ if (sha256(destination) !== bundleSha256) throw new Error('staged verified bundle digest mismatch');
120
+ return destination;
56
121
  }
57
122
 
58
123
  async function download(url, destination, headers = {}) {
@@ -134,17 +199,32 @@ export function assertInstalledPayload(sourceRoot, installedRoot) {
134
199
  return checked;
135
200
  }
136
201
 
137
- function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
202
+ export function rpcSearch(server, env, query, k = 5, timeoutMs = DEADLINE_MS, {
203
+ requiredRepo = 'ruvnet-brain',
204
+ } = {}) {
205
+ if (!Number.isSafeInteger(k) || k < 1 || k > 50) throw new Error(`invalid search result count: ${k}`);
138
206
  return new Promise((resolve, reject) => {
139
207
  const child = spawn(process.execPath, [server], { env, stdio: ['pipe', 'pipe', 'pipe'] });
140
208
  let stdout = '';
141
209
  let stderr = '';
142
210
  let timer;
211
+ let killTimer;
212
+ let closeTimer;
213
+ let outcome;
143
214
  const pending = new Map();
144
215
  const finish = (error, value) => {
216
+ if (outcome) return;
217
+ outcome = { error, value };
145
218
  clearTimeout(timer);
219
+ pending.clear();
220
+ // Do not let the next doctor/search probe contend with this process while it exits.
221
+ // A child that ignores graceful shutdown must not leave the verifier hanging either.
222
+ killTimer = setTimeout(() => {
223
+ outcome.error = new Error('installed Brain did not shut down within 5000ms');
224
+ child.kill('SIGKILL');
225
+ closeTimer = setTimeout(() => reject(new Error('installed Brain process did not close after SIGKILL')), 1000);
226
+ }, 5000);
146
227
  child.kill('SIGTERM');
147
- error ? reject(error) : resolve(value);
148
228
  };
149
229
  timer = setTimeout(() => finish(new Error(`installed Brain exceeded ${timeoutMs}ms deadline`)), timeoutMs);
150
230
  child.stderr.on('data', (chunk) => { stderr += chunk; });
@@ -162,8 +242,12 @@ function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
162
242
  }
163
243
  });
164
244
  child.on('error', (error) => finish(error));
165
- child.on('exit', (code) => {
166
- if (pending.size) finish(new Error(`installed Brain exited ${code}: ${stderr.slice(0, 400)}`));
245
+ child.on('close', (code) => {
246
+ clearTimeout(timer);
247
+ clearTimeout(killTimer);
248
+ clearTimeout(closeTimer);
249
+ const result = outcome || { error: new Error(`installed Brain exited ${code}: ${stderr.slice(0, 400)}`) };
250
+ result.error ? reject(result.error) : resolve(result.value);
167
251
  });
168
252
  const call = (id, method, params = {}) => new Promise((done) => {
169
253
  pending.set(id, done);
@@ -175,17 +259,21 @@ function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
175
259
  const listed = await call(2, 'tools/list');
176
260
  if (!listed.result?.tools?.some((tool) => tool.name === 'search_ruvnet')) throw new Error('installed MCP does not advertise search_ruvnet');
177
261
  const started = performance.now();
178
- const searched = await call(3, 'tools/call', { name: 'search_ruvnet', arguments: { query, k: 5 } });
262
+ const searched = await call(3, 'tools/call', { name: 'search_ruvnet', arguments: { query, k } });
179
263
  const broadMs = Math.round(performance.now() - started);
180
264
  const text = (searched.result?.content || []).map((item) => item.text || '').join('\n');
181
265
  if (searched.error || searched.result?.isError || /search_ruvnet error:/i.test(text)) throw new Error(`installed Brain search failed: ${text.slice(0, 400)}`);
182
- if (!/repo=ruvnet-brain/i.test(text) || !/path\s*:/i.test(text)) throw new Error('installed Brain search returned no ruvnet-brain source citation');
183
- finish(null, { broadMs, text });
266
+ if (!/repo=/i.test(text) || !/path\s*:/i.test(text)) throw new Error('installed Brain search returned no source citation');
267
+ const repos = [...text.matchAll(/repo=([\w.-]+)/gi)].map((match) => match[1].toLowerCase());
268
+ if (requiredRepo && !repos.includes(String(requiredRepo).toLowerCase())) {
269
+ throw new Error(`installed Brain search returned no ${requiredRepo} source citation`);
270
+ }
271
+ finish(null, { broadMs, text, mcpResult: searched.result });
184
272
  })().catch((error) => finish(error));
185
273
  });
186
274
  }
187
275
 
188
- export function livePublicationAdapter({ root = process.cwd() } = {}) {
276
+ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = root } = {}) {
189
277
  const installContexts = new Map();
190
278
  const passageFileDigests = new Map();
191
279
  let installTemp = null;
@@ -213,14 +301,46 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
213
301
  return { path: destination, tag: release.tag_name, sha: tagCommit(root, tag) };
214
302
  },
215
303
 
216
- async installHosts({ artifactPath, artifactSha256, version }) {
217
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-install-'));
304
+ async runNativeNightly({ identity, workflowRunId }) {
305
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-nightly-'));
306
+ let completed = false;
307
+ try {
308
+ const npm = await this.downloadNpm({ version: identity.version,
309
+ destination: path.join(temp, 'package.tgz') });
310
+ const bundle = await this.downloadGithub({ tag: identity.tag, assetName: 'ruvnet-brain.zip',
311
+ destination: path.join(temp, 'ruvnet-brain.zip') });
312
+ const signature = await this.downloadGithub({ tag: identity.tag, assetName: 'ruvnet-brain.zip.sig',
313
+ destination: path.join(temp, 'ruvnet-brain.zip.sig') });
314
+ if (sha256(npm.path) !== identity.packageSha256 || sha256(bundle.path) !== identity.bundleSha256
315
+ || npm.version !== identity.version || bundle.sha !== identity.candidateSha
316
+ || signature.sha !== identity.candidateSha) throw new Error('nightly public artifact identity mismatch');
317
+ const proof = await runNightlyTwoRunProof({ packagePath: npm.path, bundlePath: bundle.path,
318
+ signaturePath: signature.path, sourceSha: identity.candidateSha, workflowRunId,
319
+ out: path.join(temp, 'nightly-proof.json') });
320
+ const validation = validateNightlyProofReceipt(proof, { platform: process.platform, version: identity.version,
321
+ packageSha256: identity.packageSha256, bundleSha256: identity.bundleSha256,
322
+ sourceSha: identity.candidateSha, workflowRunId });
323
+ if (!validation.ok) throw new Error(`public nightly proof failed: ${validation.failures.join('; ')}`);
324
+ completed = true;
325
+ return proof;
326
+ } finally {
327
+ if (completed) fs.rmSync(temp, { recursive: true, force: true });
328
+ else console.error(`Native public artifact diagnostics retained at ${temp}`);
329
+ }
330
+ },
331
+
332
+ async installHosts({ artifactPath, artifactSha256, bundlePath, bundleSha256, version }) {
333
+ const temp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-install-')));
218
334
  installTemp = temp;
219
335
  const packageRoot = path.join(temp, 'package');
220
- command('tar', ['-xzf', artifactPath, '-C', temp]);
336
+ const extraction = tarExtractionInvocation(artifactPath, temp);
337
+ command('tar', extraction.args, { cwd: extraction.cwd });
338
+ stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot });
221
339
  const sealedPlugin = path.join(packageRoot, 'plugin');
222
340
  const results = {};
223
341
  let bundle = null;
342
+ const sharedModelCache = path.join(temp, 'models-cache');
343
+ fs.mkdirSync(sharedModelCache, { recursive: true });
224
344
  // Derived from HOST_MODES, so a fourth host shape is added in ONE place and this loop
225
345
  // cannot fall behind the staged-side check the way it did.
226
346
  for (const mode of HOST_MODES.map((m) => RECEIPT_MODE_NAMES[m])) {
@@ -233,26 +353,27 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
233
353
  const env = {
234
354
  ...process.env,
235
355
  HOME: home,
356
+ ...(process.platform === 'win32' ? { USERPROFILE: home } : {}),
236
357
  CODEX_HOME: codexHome,
237
358
  RUVNET_BRAIN_HOME: brainHome,
238
359
  RUVNET_BRAIN_KB: kb,
360
+ KB_MODEL_CACHE: sharedModelCache,
239
361
  // Env comes from the ONE variant table (host-install-matrix VARIANTS.published), not a
240
362
  // second hand-written copy — this file's own header promises "ONE doctor rule and ONE mode
241
363
  // vocabulary, shared with the staged-side check", and the copy had already drifted: it
242
364
  // omitted the Codex hook-trust bypass the staged side carries, which failed the seal on
243
365
  // every release.
244
366
  ...VARIANTS.published.env({ packageRoot }),
367
+ RUVNET_STRICT_INSTALL: '0',
245
368
  CI: 'true',
246
- PATH: isolatedPath(mode, temp),
369
+ PATH: createIsolatedPath(mode, temp),
247
370
  };
248
371
  const installer = path.join(packageRoot, 'bin', 'install.mjs');
249
372
  command(process.execPath, [
250
373
  installer, '--yes', '--force', '--version', `v${version}`,
251
374
  '--no-nightly-prompt', '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline',
375
+ '--no-selfcheck', '--no-verify',
252
376
  ], { env, cwd: packageRoot, timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024 });
253
- command(process.execPath, [installer, '--doctor', '--hooks'], {
254
- env, cwd: packageRoot, timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
255
- });
256
377
 
257
378
  const verified = {};
258
379
  if (mode !== 'codexOnly') {
@@ -270,7 +391,11 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
270
391
  throw new Error(`${mode} installed public Brain bundle version mismatch`);
271
392
  }
272
393
  bundle ||= { brainVersion: source.brainVersion, releaseTag: source.releaseTag };
273
- const searched = await rpcSearch(findMcpServer(home), env, 'How does RuvNet Brain prove a public release artifact?', DEADLINE_MS);
394
+ const searched = await rpcSearch(findMcpServer(home), env,
395
+ 'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
396
+ command(process.execPath, [installer, '--doctor', '--hooks'], {
397
+ env, cwd: packageRoot, timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
398
+ });
274
399
  results[mode] = {
275
400
  status: 'PASS', doctorExit: 0, version, artifactSha256,
276
401
  functionalSearch: true, searchMs: searched.broadMs, hostsOnPath: mode,
@@ -291,55 +416,36 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
291
416
  if (rvfs.length === 0) throw new Error('installed public Brain has no ruvnet-brain self RVF');
292
417
  const server = path.join(installContext.home, '.claude', 'ruvnet-brain', 'mcp', 'server.mjs');
293
418
  if (!fs.existsSync(server)) throw new Error('installed persistent MCP server is missing');
294
- const result = await rpcSearch(server, installContext.env, 'How does RuvNet Brain prove a public release artifact?', DEADLINE_MS);
419
+ const result = await rpcSearch(server, installContext.env,
420
+ 'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
295
421
  const readiness = readJson(path.join(installContext.brainHome, 'mcp-readiness.json'));
296
422
  if (readiness.state !== 'ready') throw new Error(`installed Brain readiness is ${readiness.state || 'missing'}`);
297
423
  return { status: 'PASS', selfStore: true, broadMs: result.broadMs, deadlineMs: DEADLINE_MS };
298
424
  },
299
425
 
300
- async searchInstalled({ mode, query }) {
426
+ async searchInstalled({ mode, query, k }) {
301
427
  const context = installContexts.get(mode);
302
428
  if (!context) throw new Error(`${mode} public host is not installed`);
303
- const result = await rpcSearch(findMcpServer(context.home), context.env, query, DEADLINE_MS);
304
- return parseCitations(result.text).map((citation) => ({
305
- repo: citation.repo.toLowerCase(),
306
- path: citation.docPath,
307
- }));
429
+ // Each canary's exact expected repository/path/passage is checked by the canary validator.
430
+ const result = await rpcSearch(findMcpServer(context.home), context.env, query, k, DEADLINE_MS,
431
+ { requiredRepo: null });
432
+ return parseRetrievalResult(result.mcpResult, { query, k });
308
433
  },
309
434
 
310
435
  async resolveInstalledCitation({ mode, matched, expected }) {
311
436
  const context = installContexts.get(mode);
312
437
  if (!context) throw new Error(`${mode} public host is not installed`);
313
- if (String(matched?.repo || '').toLowerCase() !== expected.repo || matched?.path !== expected.path) {
314
- return { resolved: false };
315
- }
316
- const files = [
317
- path.join(context.kb, `${expected.repo}.passages.jsonl`),
318
- path.join(context.kb, `${expected.repo}.big.passages.jsonl`),
319
- ].filter((file) => fs.existsSync(file));
320
- for (const file of files) {
321
- const rows = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity });
322
- try {
323
- for await (const line of rows) {
324
- let record;
325
- try { record = JSON.parse(line); } catch { continue; }
326
- if (record?.path !== expected.path) continue;
327
- const passageSha256 = digest(record);
328
- if (passageSha256 !== expected.passageSha256) return { resolved: false };
329
- if (!passageFileDigests.has(file)) passageFileDigests.set(file, sha256File(file));
330
- return { resolved: true, evidence: { passageSha256, passageFileSha256: passageFileDigests.get(file) } };
331
- }
332
- } finally { rows.close(); }
333
- }
334
- return { resolved: false };
438
+ return resolveInstalledCanaryCitation({ kbDir: context.kb, matched, expected, passageFileDigests });
335
439
  },
336
440
 
337
- async probePublishedSurface({ sha }) {
338
- const head = command('git', ['rev-parse', 'HEAD'], { cwd: root });
339
- if (head !== sha) throw new Error(`published-surface probe checkout ${head} does not match candidate ${sha}`);
340
- const result = spawnSync(process.execPath, ['scripts/published-surface-probe.mjs', '--json'], {
341
- cwd: root, env: process.env, encoding: 'utf8', timeout: 600_000,
441
+ async probePublishedSurface({ sha, version }) {
442
+ const source = validateCandidateSource(candidateRoot, { sha, version });
443
+ // The verifier supplies executable checks; the candidate supplies only the released source.
444
+ const script = fileURLToPath(new URL('./published-surface-probe.mjs', import.meta.url));
445
+ const result = spawnSync(process.execPath, [script, '--json'], {
446
+ cwd: source, env: process.env, encoding: 'utf8', timeout: 600_000,
342
447
  });
448
+ validateCandidateSource(candidateRoot, { sha, version });
343
449
  let parsed;
344
450
  try { parsed = JSON.parse(String(result.stdout || '')); } catch { throw new Error(`published-surface-probe emitted invalid JSON: ${String(result.stderr || '').slice(0, 300)}`); }
345
451
  if (result.status !== 0 || parsed.verdict !== 'PASS') throw new Error(`published-surface-probe is ${parsed.verdict || `exit ${result.status}`}`);
@@ -400,7 +506,8 @@ export async function generatePublicationReceipt({
400
506
  if (npm.version !== candidate.version || (npm.sha && npm.sha !== candidate.sha)) throw new Error('npm public identity mismatch');
401
507
  if (github.tag !== candidate.tag || github.sha !== candidate.sha) throw new Error('GitHub public identity mismatch');
402
508
 
403
- const installed = await adapter.installHosts({ artifactPath: npm.path, artifactSha256: digest, version: candidate.version });
509
+ const installed = await adapter.installHosts({ artifactPath: npm.path, artifactSha256: digest,
510
+ bundlePath: githubBundle.path, bundleSha256: githubBundleDigest, version: candidate.version });
404
511
  const brain = await adapter.probeBrain({ sha: candidate.sha, artifactSha256: digest, version: candidate.version });
405
512
  const surface = await adapter.probePublishedSurface({ sha: candidate.sha, artifactSha256: digest, version: candidate.version });
406
513
  const publication = {
@@ -52,6 +52,7 @@ import fs from 'node:fs';
52
52
  import os from 'node:os';
53
53
  import path from 'node:path';
54
54
  import { spawnSync } from 'node:child_process';
55
+ import { fileURLToPath } from 'node:url';
55
56
 
56
57
  export const EXIT = Object.freeze({ PASS: 0, FAIL: 1, UNKNOWN: 4 });
57
58
 
@@ -127,6 +128,12 @@ async function probeRegistry() {
127
128
  }
128
129
 
129
130
  // ── B. EXECUTE the published package, from the registry ────────────────────────────────────────
131
+ export function npxInvocation(args, { platform = process.platform, env = process.env } = {}) {
132
+ if (platform === 'win32') return {
133
+ executable: env.ComSpec || 'cmd.exe', args: ['/d', '/c', 'npx.cmd', ...args],
134
+ };
135
+ return { executable: 'npx', args };
136
+ }
130
137
  function probeExec() {
131
138
  const home = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-probe-'));
132
139
  try {
@@ -134,7 +141,8 @@ function probeExec() {
134
141
  // (npm resolve → download → unpack → bin mapping → node parses every module it imports) while
135
142
  // touching NOTHING on the machine. `--doctor`/`--plan` reach the network and the brain dir; this
136
143
  // probe must never be the reason a surface changes.
137
- const r = spawnSync('npx', ['-y', `${PKG}@latest`, '--help'], {
144
+ const invocation = npxInvocation(['-y', `${PKG}@latest`, '--help']);
145
+ const r = spawnSync(invocation.executable, invocation.args, {
138
146
  encoding: 'utf8',
139
147
  timeout: 300_000,
140
148
  cwd: home,
@@ -237,5 +245,5 @@ export async function main() {
237
245
  return EXIT[verdict];
238
246
  }
239
247
 
240
- const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
248
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
241
249
  if (invokedDirectly) process.exit(await main());
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { execFileSync } from 'node:child_process';
5
+
6
+ export function verdictOf(results) {
7
+ if (results.some(({ status }) => ['FAIL', 'TIMEOUT', 'BLOCKED'].includes(status))) return 'FAIL';
8
+ return results.length && results.every(({ status }) => status === 'PASS') ? 'PASS' : 'UNKNOWN';
9
+ }
10
+
11
+ // A source digest includes dirty tracked files and untracked source, never ignored build outputs.
12
+ export function sourceIdentity(root) {
13
+ const git = (args) => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
14
+ let sha = null;
15
+ try { sha = git(['rev-parse', 'HEAD']).trim(); } catch { /* unborn repository */ }
16
+ const files = [...new Set(git(['ls-files', '-z', '--cached', '--others', '--exclude-standard']).split('\0').filter(Boolean))].sort();
17
+ const hash = createHash('sha256');
18
+ for (const relative of files) {
19
+ const file = path.join(root, relative);
20
+ let bytes = Buffer.from('deleted');
21
+ try {
22
+ const stat = fs.lstatSync(file);
23
+ bytes = stat.isSymbolicLink() ? Buffer.from(`symlink:${fs.readlinkSync(file)}`)
24
+ : stat.isFile() ? Buffer.concat([Buffer.from(`${stat.mode & 0o777}:`), fs.readFileSync(file)]) : Buffer.from('directory');
25
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
26
+ hash.update(JSON.stringify([relative, createHash('sha256').update(bytes).digest('hex')]));
27
+ }
28
+ return { sha, dirty: Boolean(git(['status', '--porcelain']).trim()), digest: hash.digest('hex'), files: files.length, recipe: 'git-source-bytes-v1' };
29
+ }
30
+
31
+ export async function runLanes(lanes, execute, concurrency = 2) {
32
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error('invalid QA concurrency');
33
+ const names = new Set(lanes.map(({ name }) => name));
34
+ if (names.size !== lanes.length || lanes.some((lane) => (lane.dependsOn || []).some((name) => !names.has(name)))) throw new Error('invalid QA dependency');
35
+ const pending = [...lanes], completed = new Map();
36
+ while (pending.length) {
37
+ const resources = new Set(), batch = [];
38
+ for (const lane of pending) {
39
+ if (!(lane.dependsOn || []).every((name) => completed.has(name))) continue;
40
+ if ((lane.dependsOn || []).some((name) => completed.get(name).status !== 'PASS')) {
41
+ completed.set(lane.name, { name: lane.name, status: 'BLOCKED', reason: 'required prerequisite did not pass' });
42
+ continue;
43
+ }
44
+ if (batch.length >= concurrency || (lane.resource && resources.has(lane.resource))) continue;
45
+ batch.push(lane); resources.add(lane.resource);
46
+ }
47
+ if (!batch.length && !pending.some(({ name }) => completed.has(name))) throw new Error('QA dependency cycle');
48
+ await Promise.all(batch.map(async (lane) => {
49
+ let result;
50
+ try { result = await execute(lane); }
51
+ catch (error) { result = { name: lane.name, status: 'FAIL', reason: error.message }; }
52
+ completed.set(lane.name, result);
53
+ }));
54
+ for (let index = pending.length - 1; index >= 0; index--) if (completed.has(pending[index].name)) pending.splice(index, 1);
55
+ }
56
+ return lanes.map(({ name }) => completed.get(name));
57
+ }
@@ -0,0 +1,40 @@
1
+ // Shared producers for local diagnostics, CI, and candidate qualification.
2
+ export function selectLanes(lanes, requested) {
3
+ if (!requested.length) return lanes;
4
+ const selected = new Set();
5
+ const visit = (name) => {
6
+ const lane = lanes.find((entry) => entry.name === name);
7
+ if (!lane) throw new Error(`unknown QA lane: ${name}`);
8
+ if (selected.has(name)) return;
9
+ selected.add(name);
10
+ for (const dependency of lane.dependsOn || []) visit(dependency);
11
+ };
12
+ requested.forEach(visit);
13
+ return lanes.filter(({ name }) => selected.has(name));
14
+ }
15
+
16
+ export function qaLanes({ release = false, base = null, runtimeCensusArgs = [] } = {}) {
17
+ const node = (name, script, args = [], extra = {}) => ({ name, command: process.execPath, args: [script, ...args], resource: 'static', ...extra });
18
+ const test = (name, args) => node(name, 'node_modules/vitest/vitest.mjs', ['run', ...args], { resource: 'tests' });
19
+ return [
20
+ node('version', 'scripts/sync-version.mjs', ['--check']),
21
+ node('convergence', 'scripts/convergence-manifest.mjs'),
22
+ node('execution-policy', 'scripts/execution-policy.mjs', ['{"action":"delegate","description":"architecture audit","nativeHosts":["codex"]}']),
23
+ node('architecture', 'scripts/product-integrity-contract.mjs', ['--check-source']),
24
+ node('docs', 'scripts/doc-currency.mjs', ['--check', ...(base ? ['--changed', base] : ['--warn-drift'])]),
25
+ node('wiring', 'scripts/wired-check.mjs', ['--check']),
26
+ node('substitution', 'scripts/no-silent-substitution.mjs'),
27
+ node('catalog', 'scripts/verify-model-catalog.mjs'),
28
+ test('coverage', ['tests/unit', '--coverage']),
29
+ node('claims-source', 'scripts/claims-verify.mjs', ['--strict', '--scope', 'source'], { dependsOn: ['coverage'], report: 'claims' }),
30
+ test('mesh', ['tests/mesh']),
31
+ node('plugin', 'plugin/test/run-tests.mjs', [], { resource: 'tests' }),
32
+ ...(release ? [
33
+ test('mutation', ['tests/mutation']),
34
+ test('regression', ['tests/regression']),
35
+ test('integration', ['tests/integration']),
36
+ test('continuity', ['tests/acceptance/cross-host-project-resume.test.mjs']),
37
+ node('claims-runtime', 'scripts/claims-verify.mjs', ['--strict', '--scope', 'runtime', ...runtimeCensusArgs], { report: 'claims' }),
38
+ ] : []),
39
+ ];
40
+ }