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.
- 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 +11 -2
- 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 +472 -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/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-two-run-proof.mjs +415 -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 +53 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +108 -8
- package/scripts/publication-receipt.mjs +173 -66
- 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-publication-handoff.mjs +101 -0
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/release.mjs +10 -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
|
@@ -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 {
|
|
23
|
-
import {
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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('
|
|
166
|
-
|
|
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
|
|
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
|
|
183
|
-
|
|
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
|
|
217
|
-
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
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,
|
|
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
|
|
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]) ===
|
|
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
|
+
}
|