ruvnet-brain 4.3.39 → 4.4.0
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 +2 -2
- package/bin/install.mjs +150 -42
- package/kb/brain-profile.mjs +17 -2
- package/kb/forge-update.mjs +92 -28
- package/kb/lifecycle-evidence-retention.mjs +12 -9
- package/kb/refresh-run.mjs +17 -1
- package/kb/update-storage-transaction.mjs +44 -10
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +2 -2
- package/plugin/hooks/hooks.json +1 -1
- package/plugin/scripts/capability-registry.mjs +3 -3
- package/plugin/scripts/codex-hook-wrapper.mjs +7 -2
- package/plugin/scripts/coverage-integrity.mjs +51 -4
- package/plugin/scripts/design-wall.sh +1 -0
- package/plugin/scripts/ground-before-write.sh +1 -0
- package/plugin/scripts/ground-ruvnet.sh +3 -3
- package/plugin/scripts/grounding-answer.mjs +129 -0
- package/plugin/scripts/grounding-stamp.sh +32 -31
- package/plugin/scripts/grounding-turn-evidence.mjs +99 -5
- package/plugin/scripts/grounding-turn-gate.mjs +25 -6
- package/plugin/scripts/hook-shim.mjs +3 -0
- package/plugin/scripts/kling-preflight.sh +1 -0
- package/plugin/scripts/learn-capture.sh +1 -0
- package/plugin/scripts/project-progression-sources.mjs +16 -4
- package/plugin/scripts/project-progression-store.mjs +49 -1
- package/plugin/scripts/protect-brain-state.sh +1 -0
- package/plugin/scripts/route-dispatch.sh +1 -0
- package/plugin/scripts/session-snapshot-hook.mjs +224 -38
- package/plugin/scripts/session-start-health.mjs +24 -3
- package/plugin/scripts/session-start-update-plane.mjs +1 -1
- package/plugin/scripts/update-apply.mjs +22 -2
- package/scripts/console-instances.mjs +145 -0
- package/scripts/console-runtime-identity.mjs +2 -0
- package/scripts/corpus-canary.mjs +130 -18
- package/scripts/customer-seams.mjs +84 -0
- package/scripts/customer-state-matrix.mjs +363 -0
- package/scripts/full-suite-gate.mjs +155 -0
- package/scripts/grounding-turn-replay.mjs +11 -3
- package/scripts/hook-qualify-core.mjs +346 -0
- package/scripts/hook-qualify-hosts.mjs +115 -0
- package/scripts/hook-qualify.mjs +101 -0
- package/scripts/host-cli.mjs +115 -0
- package/scripts/qe/agentic-qe-4.3.mjs +0 -1
- package/scripts/route-gold-rank.mjs +156 -0
- package/scripts/route-index-memory.mjs +51 -0
- package/scripts/route-latency-warm.mjs +123 -0
- package/scripts/wired-check.mjs +17 -3
|
@@ -42,7 +42,8 @@ import crypto from 'node:crypto';
|
|
|
42
42
|
import fs from 'node:fs';
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
import { spawn, spawnSync } from 'node:child_process';
|
|
45
|
-
import { pathToFileURL } from 'node:url';
|
|
45
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
|
+
import { addPrivateStore, resolveRuntime, resultFromRefreshReceipt, runInstallerDoor } from './customer-seams.mjs';
|
|
46
47
|
import { CANARY_VERDICT_KIND, CORPUS_TAG_PATTERN, REQUIRED_CANARY_CHECKS } from './corpus-promotion.mjs';
|
|
47
48
|
|
|
48
49
|
export const PUBLIC_API = 'https://api.github.com';
|
|
@@ -284,7 +285,7 @@ export function installCustomer({ approvedVersion, work, home, log = () => {} })
|
|
|
284
285
|
runLogged(process.execPath, [installer, '--yes', '--force', '--version', `v${approvedVersion}`, '--no-nightly-prompt',
|
|
285
286
|
'--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline', '--no-selfcheck'],
|
|
286
287
|
{ env, cwd: home, timeoutMs: 1_800_000, log });
|
|
287
|
-
return { kbDir: env.RUVNET_BRAIN_KB };
|
|
288
|
+
return { kbDir: env.RUVNET_BRAIN_KB, home };
|
|
288
289
|
}
|
|
289
290
|
|
|
290
291
|
function runUpdater({ kbDir, env, resultFile }) {
|
|
@@ -310,17 +311,35 @@ async function fetchRelease({ apiBase, repo, tag }) {
|
|
|
310
311
|
* installer); the unit suite hands in a customer tree built offline so the updater, the judge and the
|
|
311
312
|
* verdict run for real without a 555 MB download.
|
|
312
313
|
*/
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
314
|
+
/**
|
|
315
|
+
* THE CUSTOMER STATES THE CANARY ASKS ABOUT. `clean` is the original (and default) case. The others are
|
|
316
|
+
* the two states 2026-09-30 showed are not covered by it (scripts/customer-state-matrix.mjs measured
|
|
317
|
+
* both on real releases): a machine carrying a private overlay, and a machine installed at an OLDER
|
|
318
|
+
* runtime that reaches the candidate through `npx ruvnet-brain@latest --update`. Each extra case is a
|
|
319
|
+
* fresh install in its own directory, run AFTER the clean case and deleted before the next, so peak disk
|
|
320
|
+
* stays one install + one apply. Its checks are the same judge's, prefixed `<case>:`; any failure is a FAIL.
|
|
321
|
+
*/
|
|
322
|
+
export const CANARY_CASES = Object.freeze({
|
|
323
|
+
clean: Object.freeze({ runtime: 'approved', door: 'updater', overlay: false }),
|
|
324
|
+
'private-overlay': Object.freeze({ runtime: 'approved', door: 'updater', overlay: true }),
|
|
325
|
+
'older-runtime': Object.freeze({ runtime: 'N-1', door: 'installer', overlay: false }),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
async function fetchReleaseList({ apiBase, repo }) {
|
|
329
|
+
const response = await fetch(`${apiBase}/repos/${repo}/releases?per_page=100`, { headers: { accept: 'application/vnd.github+json' } });
|
|
330
|
+
if (!response.ok) throw new Error(`release list returned HTTP ${response.status}`);
|
|
331
|
+
return response.json();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function runCase({ name, spec, repo, tag, approvedVersion, release, work, home, apiBase, install, now,
|
|
335
|
+
retryDelayMs, maxAttempts, log, writerRoot, approvedInstaller }) {
|
|
320
336
|
fs.mkdirSync(home, { recursive: true });
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
337
|
+
let runtime = approvedVersion;
|
|
338
|
+
if (spec.runtime !== 'approved') runtime = resolveRuntime(spec.runtime, { candidateTag: `v${approvedVersion}`, releases: await fetchReleaseList({ apiBase, repo }) });
|
|
339
|
+
const installed = await install({ approvedVersion: runtime, work, home, log });
|
|
340
|
+
const { kbDir } = installed;
|
|
341
|
+
home = installed.home || home;
|
|
342
|
+
const overlay = spec.overlay ? await addPrivateStore({ kbDir, scratch: work, writerRoot }) : null;
|
|
324
343
|
const beforeModules = installedModules(kbDir);
|
|
325
344
|
const beforeCoverage = coverageRows(path.join(kbDir, 'CORPUS-COVERAGE.json'));
|
|
326
345
|
const { before } = pointUpdaterAtCandidate({ kbDir, apiBase, repo, tag });
|
|
@@ -331,15 +350,107 @@ export async function runCanary({
|
|
|
331
350
|
let updater = { exitCode: null, output: '', attempts: 0 };
|
|
332
351
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
333
352
|
fs.rmSync(resultFile, { force: true });
|
|
334
|
-
const run =
|
|
353
|
+
const run = spec.door === 'installer'
|
|
354
|
+
? await runInstallerDoor({ installer: await approvedInstaller(), env: childEnv, cwd: home })
|
|
355
|
+
: await runUpdater({ kbDir, env: childEnv, resultFile });
|
|
335
356
|
updater = { ...run, attempts: attempt };
|
|
336
|
-
log(`--- forge-update.mjs --apply (attempt ${attempt}) exit ${run.exitCode}\n${run.output}`);
|
|
357
|
+
log(`--- [${name}] ${spec.door === 'installer' ? 'install.mjs --update' : 'forge-update.mjs --apply'} (attempt ${attempt}) exit ${run.exitCode}\n${run.output}`);
|
|
337
358
|
if (run.exitCode !== NETWORK_EXIT || attempt === maxAttempts) break;
|
|
338
359
|
await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
|
|
339
360
|
}
|
|
340
|
-
updater.result =
|
|
361
|
+
updater.result = spec.door === 'installer' ? resultFromRefreshReceipt(childEnv.RUVNET_BRAIN_HOME)
|
|
362
|
+
: (fs.existsSync(resultFile) ? readJson(resultFile) : null);
|
|
341
363
|
const judged = await judgeCanary({ tag, approvedVersion, release, updater, before, beforeModules, beforeCoverage,
|
|
342
364
|
kbDir: fs.realpathSync(kbDir), home: fs.realpathSync(home), now: now() });
|
|
365
|
+
if (overlay) {
|
|
366
|
+
const after = Object.fromEntries(Object.keys(overlay.digests).map((f) => [f,
|
|
367
|
+
fs.existsSync(path.join(kbDir, f)) ? sha256File(path.join(kbDir, f)) : null]));
|
|
368
|
+
const changed = Object.keys(after).filter((f) => after[f] !== overlay.digests[f]);
|
|
369
|
+
judged.checks.push({ name: 'private-store-preserved', ok: changed.length === 0,
|
|
370
|
+
detail: changed.length ? `private files changed or lost: ${changed.join(', ')}` : `${Object.keys(after).length} private file(s) byte-identical` });
|
|
371
|
+
}
|
|
372
|
+
return { updater, checks: judged.checks, runtime };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const physical = (dir) => { try { return fs.realpathSync(dir); } catch { return path.resolve(dir); } };
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* `--installed-kb`: the clean case runs on the supplied KB (as before); every extra case gets its OWN copy
|
|
379
|
+
* of that KB as it was BEFORE the clean case touched it, under the case's work dir, so the supplied brain
|
|
380
|
+
* never gains the overlay case's private store or a rewritten PRIVATE-STORES.json.
|
|
381
|
+
*
|
|
382
|
+
* DISK: copies are reflink clones (COPYFILE_FICLONE) where the filesystem supports it (APFS, btrfs, XFS
|
|
383
|
+
* with reflink) and cost almost nothing; elsewhere (ext4, most CI runners) they are full copies. Then the
|
|
384
|
+
* peak is the supplied KB + one snapshot + one case copy (each case's copy is deleted before the next),
|
|
385
|
+
* i.e. about 3x the KB (~4 GB for a 1.3 GB brain). Hardlinks are not used: an update that rewrote a
|
|
386
|
+
* file in place would write through into the caller's brain.
|
|
387
|
+
*/
|
|
388
|
+
export function suppliedKbInstaller({ installedKb, work, cases }) {
|
|
389
|
+
const supplied = path.resolve(installedKb);
|
|
390
|
+
const snapshot = path.join(work, 'supplied-kb-snapshot');
|
|
391
|
+
const copy = (from, to) => fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true, mode: fs.constants.COPYFILE_FICLONE });
|
|
392
|
+
if (cases.length > 1) { fs.rmSync(snapshot, { recursive: true, force: true }); copy(supplied, snapshot); }
|
|
393
|
+
return ({ work: caseWork, home }) => {
|
|
394
|
+
if (path.resolve(caseWork) === path.resolve(work)) return { kbDir: supplied };
|
|
395
|
+
const kbDir = path.join(home, '.cache', 'ruvnet-brain', 'kb');
|
|
396
|
+
fs.mkdirSync(path.dirname(kbDir), { recursive: true });
|
|
397
|
+
copy(snapshot, kbDir);
|
|
398
|
+
return { kbDir, home };
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function runCanary({
|
|
403
|
+
repo, tag, approvedVersion, work, apiBase = PUBLIC_API, install = installCustomer, home = path.join(work, 'home'),
|
|
404
|
+
env = process.env, now = () => Date.now(), retryDelayMs = 30_000, maxAttempts = 3, log = () => {},
|
|
405
|
+
cases = ['clean'], writerRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
|
|
406
|
+
approvedInstaller = null,
|
|
407
|
+
}) {
|
|
408
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(String(repo || ''))) throw new Error('--repo must be owner/name');
|
|
409
|
+
if (!CORPUS_TAG_PATTERN.test(String(tag || ''))) throw new Error('--tag must be corpus-sha256-<64 hex>');
|
|
410
|
+
if (!SEMVER.test(String(approvedVersion || ''))) throw new Error('--approved-version must be X.Y.Z');
|
|
411
|
+
if (cases[0] !== 'clean' || cases.some((name) => !CANARY_CASES[name])) {
|
|
412
|
+
throw new Error(`--cases must start with clean and name only: ${Object.keys(CANARY_CASES).join(', ')}`);
|
|
413
|
+
}
|
|
414
|
+
const started = now();
|
|
415
|
+
const release = await fetchRelease({ apiBase, repo, tag });
|
|
416
|
+
// The approved package's installer — what `npx ruvnet-brain@latest` runs — fetched once, on demand.
|
|
417
|
+
let installerPath = null;
|
|
418
|
+
const resolveInstaller = approvedInstaller || (async () => {
|
|
419
|
+
if (installerPath) return installerPath;
|
|
420
|
+
const prefix = path.join(work, 'approved-npx');
|
|
421
|
+
fs.mkdirSync(prefix, { recursive: true });
|
|
422
|
+
const npm = path.join(path.dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
423
|
+
runLogged(npm, ['install', '--prefix', prefix, '--no-audit', '--no-fund', '--no-save', `ruvnet-brain@${approvedVersion}`],
|
|
424
|
+
{ env: customerEnv({ home, work }), cwd: prefix, timeoutMs: 600_000, log });
|
|
425
|
+
installerPath = path.join(prefix, 'node_modules', 'ruvnet-brain', 'bin', 'install.mjs');
|
|
426
|
+
return installerPath;
|
|
427
|
+
});
|
|
428
|
+
const common = { repo, tag, approvedVersion, release, apiBase, install, now, retryDelayMs, maxAttempts, log, writerRoot,
|
|
429
|
+
approvedInstaller: resolveInstaller };
|
|
430
|
+
let cleanKb = null;
|
|
431
|
+
const clean = await runCase({ ...common, name: 'clean', spec: CANARY_CASES.clean, work, home,
|
|
432
|
+
install: async (args) => { const installed = await install(args); cleanKb = physical(installed.kbDir); return installed; } });
|
|
433
|
+
const { updater } = clean;
|
|
434
|
+
const judged = { checks: [...clean.checks] };
|
|
435
|
+
for (const name of cases.slice(1)) {
|
|
436
|
+
const caseWork = path.join(work, `case-${name}`);
|
|
437
|
+
let outcome;
|
|
438
|
+
try {
|
|
439
|
+
// An extra case adds a private store and rewrites PRIVATE-STORES.json: it must never do that to the
|
|
440
|
+
// clean case's KB (with --installed-kb, the caller's own brain). Each case gets its own tree.
|
|
441
|
+
const caseInstall = async (args) => {
|
|
442
|
+
const installed = await install(args);
|
|
443
|
+
if (physical(installed.kbDir) === cleanKb) throw new Error(`case ${name} was handed the clean case's KB (${installed.kbDir}); refusing to mutate it`);
|
|
444
|
+
return installed;
|
|
445
|
+
};
|
|
446
|
+
outcome = await runCase({ ...common, install: caseInstall, name, spec: CANARY_CASES[name], work: caseWork, home: path.join(caseWork, 'home') });
|
|
447
|
+
} catch (error) {
|
|
448
|
+
outcome = { checks: [{ name: 'case-ran', ok: false, detail: error.message }] };
|
|
449
|
+
}
|
|
450
|
+
for (const entry of outcome.checks) judged.checks.push({ ...entry, name: `${name}:${entry.name}` });
|
|
451
|
+
fs.rmSync(caseWork, { recursive: true, force: true });
|
|
452
|
+
}
|
|
453
|
+
judged.verdict = judged.checks.every((entry) => entry.ok) ? 'PASS' : 'FAIL';
|
|
343
454
|
return {
|
|
344
455
|
schemaVersion: 1,
|
|
345
456
|
kind: CANARY_VERDICT_KIND,
|
|
@@ -365,18 +476,19 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
365
476
|
const work = opt('--work') && path.resolve(opt('--work'));
|
|
366
477
|
const verdictOut = opt('--verdict-out') && path.resolve(opt('--verdict-out'));
|
|
367
478
|
if (!work || !verdictOut) {
|
|
368
|
-
process.stderr.write('usage: corpus-canary.mjs --repo o/n --tag corpus-sha256-<hex> --approved-version X.Y.Z --work <dir> --verdict-out <file> [--api-base URL] [--installed-kb <dir> --home <dir>]\n');
|
|
479
|
+
process.stderr.write('usage: corpus-canary.mjs --repo o/n --tag corpus-sha256-<hex> --approved-version X.Y.Z --work <dir> --verdict-out <file> [--api-base URL] [--installed-kb <dir> --home <dir>] [--cases clean,private-overlay,older-runtime]\n');
|
|
369
480
|
return 2;
|
|
370
481
|
}
|
|
371
482
|
fs.mkdirSync(work, { recursive: true });
|
|
372
483
|
const logFile = path.join(work, 'canary.log');
|
|
373
484
|
const log = (text) => { fs.appendFileSync(logFile, `${text}\n`); };
|
|
374
485
|
const installedKb = opt('--installed-kb');
|
|
375
|
-
const
|
|
486
|
+
const cases = String(opt('--cases') || 'clean').split(',').map((name) => name.trim()).filter(Boolean);
|
|
487
|
+
const install = installedKb ? suppliedKbInstaller({ installedKb, work, cases }) : installCustomer;
|
|
376
488
|
let record;
|
|
377
489
|
try {
|
|
378
490
|
record = await runCanary({ repo: opt('--repo'), tag: opt('--tag'), approvedVersion: opt('--approved-version'), work,
|
|
379
|
-
apiBase: opt('--api-base') || PUBLIC_API, install, home: path.resolve(opt('--home') || path.join(work, 'home')), log });
|
|
491
|
+
apiBase: opt('--api-base') || PUBLIC_API, install, home: path.resolve(opt('--home') || path.join(work, 'home')), log, cases });
|
|
380
492
|
} catch (error) {
|
|
381
493
|
// Could not even reach a judgement = the consumer did not accept. Still a written verdict.
|
|
382
494
|
record = { schemaVersion: 1, kind: CANARY_VERDICT_KIND, verdict: 'FAIL', repo: opt('--repo') || null, tag: opt('--tag') || null,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// scripts/customer-seams.mjs — the customer-side seams shared by scripts/corpus-canary.mjs (the nightly
|
|
2
|
+
// consumer gate) and scripts/customer-state-matrix.mjs (the same seams across many machine states). One copy.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
10
|
+
const sha256File = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
11
|
+
|
|
12
|
+
const SEMVER_TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
|
|
13
|
+
/** `N-k`: the k-th published (non-draft) semver code release behind `candidateTag`, from a live release list. */
|
|
14
|
+
export function resolveRuntime(offset, { candidateTag, releases }) {
|
|
15
|
+
const match = /^N-(\d+)$/.exec(offset);
|
|
16
|
+
if (!match) throw new Error(`runtime offset must be N-<k>: ${offset}`);
|
|
17
|
+
const key = (tag) => SEMVER_TAG.exec(tag).slice(1).map(Number);
|
|
18
|
+
const cmp = (a, b) => { const [x, y] = [key(a), key(b)]; return y[0] - x[0] || y[1] - x[1] || y[2] - x[2]; };
|
|
19
|
+
const tags = releases.filter((r) => !r.draft && SEMVER_TAG.test(r.tag_name || '')).map((r) => r.tag_name).sort(cmp);
|
|
20
|
+
const at = tags.indexOf(candidateTag);
|
|
21
|
+
if (at < 0) throw new Error(`candidate ${candidateTag} is not a published code release`);
|
|
22
|
+
const tag = tags[at + Number(match[1])];
|
|
23
|
+
if (!tag) throw new Error(`no published release ${offset} behind ${candidateTag}`);
|
|
24
|
+
return tag.slice(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A customer's PRIVATE store: the smallest real public store family, renamed, stamped by the product's
|
|
29
|
+
* own writer (scripts/private-overlay.mjs) with an alias and a capability card — the same shape the
|
|
30
|
+
* owner's 108 private stores have. Exercises capture -> candidate restore -> coverage convergence.
|
|
31
|
+
*/
|
|
32
|
+
export async function addPrivateStore({ kbDir, scratch, writerRoot, name = 'acme-private-notes' }) {
|
|
33
|
+
const ledger = readJson(path.join(kbDir, 'RVF-GENERATIONS.json')).stores;
|
|
34
|
+
const donor = Object.entries(ledger).filter(([, g]) => /\.big\.rvf$/.test(g.file || ''))
|
|
35
|
+
.sort(([, a], [, b]) => a.bytes - b.bytes)[0][0];
|
|
36
|
+
const from = path.join(scratch, 'private-sidecars');
|
|
37
|
+
fs.mkdirSync(from, { recursive: true });
|
|
38
|
+
for (const file of fs.readdirSync(kbDir).filter((f) => f.startsWith(`${donor}.`) || f === `${donor}-primer.md`)) {
|
|
39
|
+
fs.copyFileSync(path.join(kbDir, file), path.join(from, file.replace(donor, name)));
|
|
40
|
+
}
|
|
41
|
+
const { applyPrivateOverlay } = await import(pathToFileURL(path.join(writerRoot, 'scripts', 'private-overlay.mjs')).href);
|
|
42
|
+
const card = path.join(scratch, `${name}.card.md`);
|
|
43
|
+
fs.writeFileSync(card, `Private notes (a renamed copy of ${donor}). Reach for it only for this customer's own material.\n`);
|
|
44
|
+
// The fence comes first, exactly as the writer demands of a customer (PRIVATE-STORES.json).
|
|
45
|
+
const fenceFile = path.join(kbDir, 'PRIVATE-STORES.json');
|
|
46
|
+
const fence = fs.existsSync(fenceFile) ? readJson(fenceFile) : { privateStores: [] };
|
|
47
|
+
fs.writeFileSync(fenceFile, `${JSON.stringify({ ...fence, privateStores: [...new Set([...(fence.privateStores || []), name])] }, null, 2)}\n`);
|
|
48
|
+
applyPrivateOverlay({ root: kbDir, from, stores: [name], aliases: { [name]: [`${name}-alias`] }, cards: { [name]: card } });
|
|
49
|
+
const digests = Object.fromEntries(fs.readdirSync(kbDir).filter((f) => f.startsWith(`${name}.`) || f.startsWith(`${name}-`))
|
|
50
|
+
.sort().map((f) => [f, sha256File(path.join(kbDir, f))]));
|
|
51
|
+
return { name, donor, digests };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The `npx ruvnet-brain@latest --update` door: the APPROVED package's own installer, which self-upgrades
|
|
56
|
+
* the installed updater, re-stamps the runtime, takes the refresh lock and runs the updater. Fallback is
|
|
57
|
+
* disabled for the same reason the clean case never uses this door: a refused candidate must not turn
|
|
58
|
+
* into a green fresh install. The updater's receipt is reconstructed from the refresh-run receipt the
|
|
59
|
+
* installer settles (the installer deletes its private result file).
|
|
60
|
+
*/
|
|
61
|
+
export function runInstallerDoor({ installer, env, cwd }) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const child = spawn(process.execPath, [installer, '--update', '--no-nightly-prompt'],
|
|
64
|
+
{ cwd, env: { ...env, RUVNET_BRAIN_NO_UPDATE_FALLBACK: '1' }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
65
|
+
let output = '';
|
|
66
|
+
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
67
|
+
child.stderr.on('data', (chunk) => { output += chunk; });
|
|
68
|
+
child.on('error', (error) => resolve({ exitCode: null, output: `${output}\n${error.message}` }));
|
|
69
|
+
child.on('close', (exitCode) => resolve({ exitCode, output }));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resultFromRefreshReceipt(brainHome) {
|
|
74
|
+
const dir = path.join(brainHome, 'refresh-runs');
|
|
75
|
+
if (!fs.existsSync(dir)) return null;
|
|
76
|
+
const newest = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => path.join(dir, f))
|
|
77
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
|
|
78
|
+
if (!newest) return null;
|
|
79
|
+
const receipt = readJson(newest);
|
|
80
|
+
const phase = (name) => receipt.phases?.find((p) => p.phase === name)?.evidence || {};
|
|
81
|
+
return { terminalVerdict: receipt.status === 'SUCCEEDED' ? phase('update').terminalVerdict || null : receipt.terminalVerdict,
|
|
82
|
+
bundleSha256: phase('bundle-assembly').bundleSha256 || null, coverageSha256: phase('coverage-generation').coverageSha256 || null,
|
|
83
|
+
refreshStatus: receipt.status };
|
|
84
|
+
}
|