ruvnet-brain 4.6.0 → 4.6.2
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 +243 -510
- package/bin/nightly-refresh.mjs +5 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/developer-update-maintenance.mjs +133 -6
- package/plugin/scripts/developer-update.mjs +37 -7
- package/plugin/scripts/nightly-scheduler.mjs +27 -6
- package/scripts/release-qualification-contract.mjs +8 -3
package/bin/nightly-refresh.mjs
CHANGED
|
@@ -80,7 +80,11 @@ try {
|
|
|
80
80
|
if (registration.mode === 'developer-suite') {
|
|
81
81
|
const coordinator = await import(pathToFileURL(modules['developer-update.mjs'].path).href);
|
|
82
82
|
const receipt = await coordinator.runDeveloperUpdate({ mode: 'apply', env: { ...process.env,
|
|
83
|
-
RUVNET_NIGHTLY_IDENTITY: registration.identity, RUVNET_NIGHTLY: '1'
|
|
83
|
+
RUVNET_NIGHTLY_IDENTITY: registration.identity, RUVNET_NIGHTLY: '1',
|
|
84
|
+
RUVNET_NIGHTLY_REGISTRATION: registrationPath,
|
|
85
|
+
RUVNET_NIGHTLY_NODE_PATH: registration.nodePath,
|
|
86
|
+
RUVNET_NIGHTLY_RUNNER_PATH: registration.runnerPath,
|
|
87
|
+
RUVNET_NIGHTLY_RUNNER_SHA256: registration.runnerSha256 } });
|
|
84
88
|
console.log(JSON.stringify(receipt));
|
|
85
89
|
process.exit(0);
|
|
86
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.2",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
3
|
"description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
-
"version": "4.6.
|
|
4
|
+
"version": "4.6.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -3,6 +3,114 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { cmpVersion as compare } from './developer-update-policy.mjs';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
const quote = text => `'${String(text).replaceAll("'", "'\\''")}'`;
|
|
10
|
+
function regular(file) {
|
|
11
|
+
const stat = fs.lstatSync(file);
|
|
12
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid?.()
|
|
13
|
+
|| (stat.mode & 0o022)) throw Error('Codex launcher requires owned regular files');
|
|
14
|
+
return fs.readFileSync(file);
|
|
15
|
+
}
|
|
16
|
+
function runtimeIdentity(root) {
|
|
17
|
+
const entries = [];
|
|
18
|
+
function visit(relative) {
|
|
19
|
+
const file = path.join(root, relative), stat = fs.lstatSync(file);
|
|
20
|
+
if (stat.isDirectory() && !stat.isSymbolicLink() && stat.uid === process.getuid?.()) {
|
|
21
|
+
for (const name of fs.readdirSync(file)) visit(path.join(relative, name));
|
|
22
|
+
} else entries.push([relative.split(path.sep).join('/'), sha(regular(file))]);
|
|
23
|
+
}
|
|
24
|
+
visit('');
|
|
25
|
+
return sha(JSON.stringify(entries.sort(([a], [b]) => a.localeCompare(b))));
|
|
26
|
+
}
|
|
27
|
+
function nativeOwner(file, home) {
|
|
28
|
+
const real = fs.realpathSync(file), root = path.join(home, '.codex/packages/standalone/releases') + path.sep;
|
|
29
|
+
if (!real.startsWith(root) || path.basename(real) !== 'codex') throw Error('Codex native owner outside standalone releases');
|
|
30
|
+
const bytes = regular(real), magic = bytes.subarray(0, 4).toString('hex');
|
|
31
|
+
if (!['7f454c46', 'cffaedfe', 'feedfacf', 'cafebabe', 'bebafeca'].includes(magic)) throw Error('Codex native executable identity unavailable');
|
|
32
|
+
fs.accessSync(real, fs.constants.X_OK);
|
|
33
|
+
return real;
|
|
34
|
+
}
|
|
35
|
+
export function inspectCodexLauncher(file, home) {
|
|
36
|
+
if (file !== path.join(home, '.local/bin/codex') || fs.lstatSync(file).isSymbolicLink()) return null;
|
|
37
|
+
const bytes = regular(file);
|
|
38
|
+
// A direct native binary is handled by the ordinary owner gate; unknown scripts never run.
|
|
39
|
+
if (!bytes.subarray(0, 2).equals(Buffer.from('#!'))) {
|
|
40
|
+
if (!['7f454c46', 'cffaedfe', 'feedfacf', 'cafebabe', 'bebafeca'].includes(bytes.subarray(0, 4).toString('hex'))) throw Error('Unknown Codex native launcher refused');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const configPath = path.join(home, '.cache/ruvnet-brain/model-routing/terminal-launcher-config.json');
|
|
44
|
+
const configBytes = regular(configPath), config = JSON.parse(configBytes);
|
|
45
|
+
const runtimeRoot = path.join(home, '.cache/ruvnet-brain/model-routing/versions', config.runtimeDigest || '');
|
|
46
|
+
const keys = ['schemaVersion', 'managedBy', 'runtimeDigest', 'runtimeRoot', 'nodeBinary', 'runner', 'realCodex', 'gatewayPath',
|
|
47
|
+
...(config.realClaude ? ['realClaude', 'claudeHelperPath', 'enginePath'] : [])];
|
|
48
|
+
if (Object.keys(config).sort().join(',') !== keys.sort().join(',')) throw Error('Codex launcher config shape mismatch');
|
|
49
|
+
if (config.schemaVersion !== 1 || config.managedBy !== 'ruvnet-brain-terminal-launchers'
|
|
50
|
+
|| !/^[a-f0-9]{64}$/.test(config.runtimeDigest || '') || config.runtimeRoot !== runtimeRoot
|
|
51
|
+
|| fs.realpathSync(runtimeRoot) !== runtimeRoot || config.runner !== path.join(runtimeRoot, 'scripts/model-terminal-launchers.mjs')
|
|
52
|
+
|| config.gatewayPath !== path.join(runtimeRoot, 'scripts/model-terminal-gateway.mjs')
|
|
53
|
+
|| (config.realClaude && (config.claudeHelperPath !== path.join(runtimeRoot, 'scripts/claude-terminal-mod.mjs')
|
|
54
|
+
|| config.enginePath !== path.join(runtimeRoot, 'scripts/model-router-engine.mjs')))
|
|
55
|
+
|| runtimeIdentity(runtimeRoot) !== config.runtimeDigest) throw Error('Codex launcher runtime/config identity mismatch');
|
|
56
|
+
const expected = `#!/bin/sh\n# Managed RuvNet Brain terminal launcher\nexec ${quote(config.nodeBinary)} ${quote(config.runner)} --launch 'codex' --config ${quote(configPath)} -- "$@"\n`;
|
|
57
|
+
if (!bytes.equals(Buffer.from(expected))) throw Error('Preserving unknown Codex terminal wrapper');
|
|
58
|
+
regular(config.nodeBinary); fs.accessSync(config.nodeBinary, fs.constants.X_OK);
|
|
59
|
+
// The shared installer also visits registered Claude launchers; require exact existing bytes.
|
|
60
|
+
const claudeEntries = [];
|
|
61
|
+
if (config.realClaude) {
|
|
62
|
+
const claudeBytes = Buffer.from(expected.replace("--launch 'codex'", "--launch 'claude'"));
|
|
63
|
+
for (const entry of ['claude', 'ruvnet-brain-claude-terminal']) {
|
|
64
|
+
const claudeFile = path.join(home, '.local/bin', entry);
|
|
65
|
+
if (!regular(claudeFile).equals(claudeBytes)) throw Error('Preserving changed Claude terminal wrapper');
|
|
66
|
+
claudeEntries.push({ file: claudeFile, bytes: claudeBytes });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { file, home, configPath, configBytes, config, bytes, claudeEntries, native: nativeOwner(config.realCodex, home) };
|
|
70
|
+
}
|
|
71
|
+
export function updatedCodexNative(proof) {
|
|
72
|
+
if (fs.lstatSync(proof.file).uid !== process.getuid?.()) throw Error('Codex canonical owner changed during update');
|
|
73
|
+
if (!fs.lstatSync(proof.file).isSymbolicLink() && regular(proof.file).equals(proof.bytes)) {
|
|
74
|
+
return nativeOwner(path.join(proof.home, '.codex/packages/standalone/current/bin/codex'), proof.home);
|
|
75
|
+
}
|
|
76
|
+
return nativeOwner(proof.file, proof.home);
|
|
77
|
+
}
|
|
78
|
+
export async function restoreCodexLauncher(proof, { retainOriginalNative = false } = {}) {
|
|
79
|
+
const { file, home, config, configPath } = proof;
|
|
80
|
+
if (!regular(configPath).equals(proof.configBytes) || runtimeIdentity(config.runtimeRoot) !== config.runtimeDigest) {
|
|
81
|
+
throw Error('Codex launcher identity changed during native update');
|
|
82
|
+
}
|
|
83
|
+
for (const entry of proof.claudeEntries) if (!regular(entry.file).equals(entry.bytes)) throw Error('Claude wrapper changed during Codex update');
|
|
84
|
+
const candidate = updatedCodexNative(proof);
|
|
85
|
+
const native = retainOriginalNative ? nativeOwner(proof.native, home) : candidate;
|
|
86
|
+
const installed = await import(pathToFileURL(config.runner).href);
|
|
87
|
+
if (typeof installed.installTerminalLaunchers !== 'function') throw Error('Codex launcher installer unavailable');
|
|
88
|
+
// Installer accepts only same-owner native links. On failure retain the known original
|
|
89
|
+
// wrapper first, backing up the verified vendor link, then let its installer reconcile config.
|
|
90
|
+
if (retainOriginalNative && fs.lstatSync(file).isSymbolicLink()) {
|
|
91
|
+
const link = fs.readlinkSync(file), backup = `${file}.${sha(link)}.original`;
|
|
92
|
+
try { fs.symlinkSync(link, backup); }
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code !== 'EEXIST' || !fs.lstatSync(backup).isSymbolicLink()
|
|
95
|
+
|| fs.lstatSync(backup).uid !== process.getuid?.() || fs.readlinkSync(backup) !== link) throw Error('Codex vendor link backup mismatch');
|
|
96
|
+
}
|
|
97
|
+
const temporary = `${file}.${crypto.randomUUID()}.tmp`;
|
|
98
|
+
try { fs.writeFileSync(temporary, proof.bytes, { flag: 'wx', mode: 0o755 }); fs.renameSync(temporary, file); }
|
|
99
|
+
finally { fs.rmSync(temporary, { force: true }); }
|
|
100
|
+
}
|
|
101
|
+
installed.installTerminalLaunchers({ home, runtimeRoot: config.runtimeRoot, runtimeDigest: config.runtimeDigest,
|
|
102
|
+
nodeBinary: config.nodeBinary, realCodex: native, realClaude: config.realClaude, apply: true, manageZsh: false });
|
|
103
|
+
if (!regular(file).equals(proof.bytes)) throw Error('Codex wrapper restoration verification failed');
|
|
104
|
+
const after = JSON.parse(regular(configPath));
|
|
105
|
+
const expectedConfig = { ...config, realCodex: native };
|
|
106
|
+
const sameConfig = Object.keys(after).sort().join(',') === Object.keys(expectedConfig).sort().join(',')
|
|
107
|
+
&& Object.entries(expectedConfig).every(([key, value]) => after[key] === value);
|
|
108
|
+
if (!sameConfig || nativeOwner(after.realCodex, home) !== native) {
|
|
109
|
+
throw Error('Codex restored owner verification failed');
|
|
110
|
+
}
|
|
111
|
+
return native;
|
|
112
|
+
}
|
|
113
|
+
|
|
6
114
|
const HOME = os.homedir();
|
|
7
115
|
const read = p => JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
8
116
|
function required(p) { fs.accessSync(p, fs.constants.X_OK); return p; }
|
|
@@ -66,14 +174,33 @@ export async function maintenance(config, run, dryRun, { home = HOME, brainHome,
|
|
|
66
174
|
const resolved = fs.realpathSync(file);
|
|
67
175
|
if (resolved.split(path.sep).includes('node_modules')) { result.exclusions.push(`npm owns native alias: ${file}`); continue; }
|
|
68
176
|
required(file);
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
177
|
+
const launcher = inspectCodexLauncher(file, home);
|
|
178
|
+
const command = launcher?.native || file;
|
|
179
|
+
const owner = launcher?.native || fs.realpathSync(file), stage = { owner: file, originalTarget: owner, state: 'running', commands: [] }; result.stages.push(stage);
|
|
180
|
+
if (launcher) stage.launcherProof = { kind: 'brain-terminal-wrapper', runtimeDigest: launcher.config.runtimeDigest, wrapperSha256: sha(launcher.bytes) };
|
|
181
|
+
const before = commandOutput(run, command, ['--version'], { timeout: 30_000 }, stage); stage.before = before;
|
|
73
182
|
const version = text => text.match(/\b(\d+\.\d+\.\d+(?:-[\w.-]+)?)/)?.[1];
|
|
74
|
-
|
|
183
|
+
let after, afterOwner, versionVerified, restorationAttempted = false;
|
|
184
|
+
try {
|
|
185
|
+
if (!dryRun) commandOutput(run, command, args, { timeout: 900_000 }, stage);
|
|
186
|
+
const afterCommand = launcher && !dryRun ? updatedCodexNative(launcher) : command;
|
|
187
|
+
after = commandOutput(run, afterCommand, ['--version'], { timeout: 30_000 }, stage);
|
|
188
|
+
versionVerified = !!version(before) && !!version(after) && compare(version(after), version(before)) >= 0;
|
|
189
|
+
if (launcher && !dryRun) {
|
|
190
|
+
restorationAttempted = true;
|
|
191
|
+
if (!versionVerified) stage.rejectedNativeTarget = afterCommand;
|
|
192
|
+
afterOwner = await restoreCodexLauncher(launcher, { retainOriginalNative: !versionVerified });
|
|
193
|
+
}
|
|
194
|
+
else afterOwner = launcher?.native || fs.realpathSync(file);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
stage.state = 'failed';
|
|
197
|
+
if (launcher && !dryRun && !restorationAttempted) {
|
|
198
|
+
try { stage.restoredTarget = await restoreCodexLauncher(launcher, { retainOriginalNative: true }); }
|
|
199
|
+
catch (restoreError) { stage.restorationError = restoreError.message; }
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
75
203
|
// Native launchers may re-point to a newer version, but stay in the same established root.
|
|
76
|
-
const afterOwner = fs.realpathSync(file);
|
|
77
204
|
const ownerPreserved = nativeOwnerPreserved(file, owner, afterOwner, home);
|
|
78
205
|
settleStage(stage, after, { versionVerified, ownerPreserved }, { currentTarget: afterOwner });
|
|
79
206
|
}
|
|
@@ -310,7 +310,7 @@ function knowledge(run, dryRun, runId, { brainHome, root, node }) {
|
|
|
310
310
|
}
|
|
311
311
|
export function developerUpdatePaths({ home = os.homedir(), brainHome = process.env.RUVNET_BRAIN_HOME || path.join(home, '.cache/ruvnet-brain') } = {}) {
|
|
312
312
|
return { home, brainHome, config: path.join(brainHome, 'developer-update-config.json'),
|
|
313
|
-
receipt: path.join(brainHome, 'nightly-suite-update.json'), lock: path.join(brainHome, 'developer-update.lock') };
|
|
313
|
+
receipt: path.join(brainHome, 'nightly-suite-update.json'), scheduledReceipt: path.join(brainHome, 'scheduler/last-suite-attempt.json'), lock: path.join(brainHome, 'developer-update.lock') };
|
|
314
314
|
}
|
|
315
315
|
export function validateDeveloperUpdateConfig(value = {}) {
|
|
316
316
|
const defaults = { schemaVersion: 1, channel: 'latest', scope: 'ruvnet', homebrew: false, uv: false, cargo: false, native: false, managedCallback: null, cleanup: false, preservePackages: [] };
|
|
@@ -371,12 +371,42 @@ export async function runDeveloperUpdate({ mode = 'check', home = os.homedir(),
|
|
|
371
371
|
run.receipt = (command, args, options = {}) => dispatch(command, args, { ...options, capture: true });
|
|
372
372
|
const locate = name => locateExecutable(name, { env: childEnv });
|
|
373
373
|
const receipt = { schemaVersion: 1, kind: 'nightly-suite-update', runId: crypto.randomUUID(), pid: process.pid,
|
|
374
|
-
startedAt: new Date().toISOString(), mode, state: 'running', ok: false, config, steps: [], notes: [],
|
|
374
|
+
startedAt: new Date().toISOString(), mode, scheduled: mode === 'apply' && env.RUVNET_NIGHTLY === '1' && !!env.RUVNET_NIGHTLY_IDENTITY, state: 'running', ok: false, config, steps: [], notes: [],
|
|
375
375
|
sourceSha256: null, ownerToken: lock.token, ownerPid: lock.ownerPid, schedulerIdentity: env.RUVNET_NIGHTLY_IDENTITY || null };
|
|
376
|
+
const scheduledAttempt = value => value?.mode === 'apply' && value.scheduled !== false && !!value.schedulerIdentity;
|
|
377
|
+
const readAttempt = file => {
|
|
378
|
+
try {
|
|
379
|
+
const stat = fs.lstatSync(file);
|
|
380
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw Error('scheduled receipt must be a regular file');
|
|
381
|
+
return read(file);
|
|
382
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
383
|
+
};
|
|
384
|
+
const persistReceipt = () => {
|
|
385
|
+
// Write Activity first: if the second write fails, health still sees this newer failed attempt.
|
|
386
|
+
atomic(paths.receipt, receipt);
|
|
387
|
+
if (receipt.scheduled) {
|
|
388
|
+
try { atomic(paths.scheduledReceipt, receipt); }
|
|
389
|
+
catch (error) {
|
|
390
|
+
receipt.state = 'failed'; receipt.ok = false; receipt.error = `Scheduled receipt persistence failed: ${error.message}`;
|
|
391
|
+
atomic(paths.receipt, receipt);
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
let latestMayBeReplaced = receipt.scheduled;
|
|
376
397
|
try {
|
|
398
|
+
if (!receipt.scheduled) {
|
|
399
|
+
const latest = readAttempt(paths.receipt), retained = readAttempt(paths.scheduledReceipt);
|
|
400
|
+
const latestTime = Date.parse(latest?.startedAt || latest?.finishedAt || ''), retainedTime = Date.parse(retained?.startedAt || retained?.finishedAt || '');
|
|
401
|
+
if (scheduledAttempt(latest) && (!retained || !Number.isFinite(latestTime) || !Number.isFinite(retainedTime) || latestTime >= retainedTime)) {
|
|
402
|
+
// Preserve real legacy/failed evidence before a check or manual run replaces Activity.
|
|
403
|
+
atomic(paths.scheduledReceipt, latest);
|
|
404
|
+
}
|
|
405
|
+
latestMayBeReplaced = true;
|
|
406
|
+
}
|
|
377
407
|
receipt.sourceSha256 = hash(fs.readFileSync(new URL(import.meta.url)));
|
|
378
408
|
receipt.sourceSnapshot = Object.fromEntries(EXECUTION_MODULES.map(name => [name, hash(fs.readFileSync(new URL(name, import.meta.url)))]));
|
|
379
|
-
|
|
409
|
+
persistReceipt();
|
|
380
410
|
const npm = locate('npm');
|
|
381
411
|
if (!npm) throw Error('existing npm owner absent; fresh install refused');
|
|
382
412
|
const prefix = run(npm, ['prefix', '-g'], { timeout: 30_000 }).trim();
|
|
@@ -386,16 +416,16 @@ export async function runDeveloperUpdate({ mode = 'check', home = os.homedir(),
|
|
|
386
416
|
childEnv.npm_config_prefix = prefix;
|
|
387
417
|
receipt.npmIdentity = { npm, prefix, root, node: process.execPath };
|
|
388
418
|
receipt.before = discover(root, prefix, config.scope);
|
|
389
|
-
|
|
419
|
+
persistReceipt();
|
|
390
420
|
for (const before of receipt.before) {
|
|
391
421
|
const tags = before.localSource ? {} : JSON.parse(run(npm, ['view', before.name, 'dist-tags', '--json'], { timeout: 90_000 }));
|
|
392
422
|
receipt.steps.push(upgradePackage(before, tags, REVIEWED_INSTALL_SCRIPTS, { run, dryRun: mode === 'check', channel: config.channel, npm, preservePackages: config.preservePackages }));
|
|
393
|
-
|
|
423
|
+
persistReceipt();
|
|
394
424
|
}
|
|
395
425
|
receipt.plugins = await synchronizePlugins(run, mode === 'check', receipt.notes, { home, prefix, scope: config.scope, locate, unattended: !!env.RUVNET_NIGHTLY_IDENTITY });
|
|
396
426
|
if (exists(path.join(paths.brainHome, 'kb/forge-update.mjs'))) receipt.knowledge = knowledge(run, mode === 'check', receipt.runId, { brainHome: paths.brainHome, root, node: process.execPath });
|
|
397
427
|
else receipt.notes.push('Brain corpus absent; fresh knowledge install excluded');
|
|
398
|
-
receipt.maintenance = await maintenance(config, run, mode === 'check', { home, brainHome: paths.brainHome, locate, node: process.execPath, progress: result => { receipt.maintenance = result;
|
|
428
|
+
receipt.maintenance = await maintenance(config, run, mode === 'check', { home, brainHome: paths.brainHome, locate, node: process.execPath, progress: result => { receipt.maintenance = result; persistReceipt(); } });
|
|
399
429
|
receipt.cleanup = cleanupNpxDuplicates({ home, globalRoot: root, enabled: config.cleanup && mode === 'apply', run });
|
|
400
430
|
receipt.after = discover(root, prefix, config.scope);
|
|
401
431
|
receipt.state = mode === 'check' ? 'checked' : 'completed'; receipt.ok = true;
|
|
@@ -412,7 +442,7 @@ export async function runDeveloperUpdate({ mode = 'check', home = os.homedir(),
|
|
|
412
442
|
throw error;
|
|
413
443
|
} finally {
|
|
414
444
|
receipt.finishedAt = new Date().toISOString();
|
|
415
|
-
try {
|
|
445
|
+
try { if (latestMayBeReplaced) persistReceipt(); } finally { lock.release(); }
|
|
416
446
|
}
|
|
417
447
|
}
|
|
418
448
|
export async function developerUpdateCli(args = process.argv.slice(2)) {
|
|
@@ -252,24 +252,45 @@ export function resolveNightlyProofBundle({ brainHome, env = process.env } = {})
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
export function developerRunHealth({ brainHome, registration, now = Date.now(), maxAgeHours = 30 }) {
|
|
255
|
-
const
|
|
256
|
-
|
|
255
|
+
const scheduled = value => value?.mode === 'apply' && value.scheduled !== false && !!value.schedulerIdentity;
|
|
256
|
+
const readReceipt = file => {
|
|
257
|
+
try {
|
|
258
|
+
const stat = fs.lstatSync(file);
|
|
259
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw Error('receipt is not a regular file');
|
|
260
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
261
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
262
|
+
};
|
|
257
263
|
let receipt;
|
|
258
|
-
try {
|
|
259
|
-
|
|
264
|
+
try {
|
|
265
|
+
const retained = readReceipt(path.join(brainHome, 'scheduler/last-suite-attempt.json'));
|
|
266
|
+
const latest = readReceipt(path.join(brainHome, 'nightly-suite-update.json'));
|
|
267
|
+
if (retained && !scheduled(retained)) throw Error('retained receipt is not a scheduled apply attempt');
|
|
268
|
+
receipt = retained || latest;
|
|
269
|
+
if (scheduled(latest)) {
|
|
270
|
+
const latestTime = Date.parse(latest.startedAt || latest.finishedAt || '');
|
|
271
|
+
const retainedTime = Date.parse(retained?.startedAt || retained?.finishedAt || '');
|
|
272
|
+
if (!retained || !Number.isFinite(latestTime) || !Number.isFinite(retainedTime) || latestTime >= retainedTime) receipt = latest;
|
|
273
|
+
}
|
|
274
|
+
} catch (error) { return { state: 'failed', evidence: `Coordinated update receipt unreadable or refused: ${error.message}`, receipt: null }; }
|
|
275
|
+
if (!receipt) return { state: 'never-ran', evidence: 'No coordinated update receipt exists yet.', receipt: null };
|
|
260
276
|
const validDigest = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
261
277
|
const bound = validDigest(receipt.sourceSha256) && receipt.kind === 'nightly-suite-update' && receipt.schemaVersion === 1
|
|
262
278
|
&& receipt.sourceSha256 === registration.updateModules?.['developer-update.mjs']?.sha256
|
|
279
|
+
&& Object.entries(receipt.sourceSnapshot || {}).every(([name, digest]) => validDigest(digest) && digest === registration.updateModules?.[name]?.sha256)
|
|
263
280
|
&& EXECUTION_MODULES.filter(name => name !== 'plugin-artifact-proof.mjs' || registration.updateModules?.[name]).every(name => validDigest(receipt.sourceSnapshot?.[name]) && receipt.sourceSnapshot[name] === registration.updateModules?.[name]?.sha256);
|
|
264
281
|
const ageHours = (now - Date.parse(receipt.finishedAt || receipt.startedAt || '')) / 3_600_000;
|
|
265
|
-
|
|
282
|
+
const times = [receipt.startedAt, receipt.finishedAt].filter(value => value !== undefined).map(value => Date.parse(value));
|
|
283
|
+
const timestampsValid = times.length > 0 && times.every(value => Number.isFinite(value) && value <= now)
|
|
284
|
+
&& !(receipt.startedAt && receipt.finishedAt && Date.parse(receipt.finishedAt) < Date.parse(receipt.startedAt));
|
|
285
|
+
if (!bound) return { state: 'failed', evidence: 'Coordinated update receipt is not bound to registered source.', receipt };
|
|
286
|
+
if (!Number.isFinite(ageHours) || !timestampsValid) return { state: 'failed', evidence: 'Coordinated update receipt timestamps are invalid or future-dated.', receipt };
|
|
287
|
+
if (!scheduled(receipt) || receipt.schedulerIdentity !== registration.identity) return { state: 'never-ran', ageHours, receipt, evidence: 'No scheduled apply attempt is bound to the current registration.' };
|
|
266
288
|
if (receipt.state === 'running') {
|
|
267
289
|
const lock = sharedLockStatus({ brainHome });
|
|
268
290
|
const live = lock.state === 'running' && lock.owner.pid === (receipt.ownerPid || receipt.pid) && lock.owner.token === receipt.ownerToken;
|
|
269
291
|
return { state: live ? 'running' : 'failed', ageHours, receipt, evidence: live ? 'Coordinated updates have a live exact owner.' : 'Coordinated update owner is absent or unverified.' };
|
|
270
292
|
}
|
|
271
293
|
if (receipt.state === 'failed' || receipt.ok !== true) return { state: 'failed', ageHours, receipt, evidence: receipt.error || 'Coordinated updates failed.' };
|
|
272
|
-
if (receipt.mode !== 'apply' || receipt.schedulerIdentity !== registration.identity) return { state: 'never-ran', ageHours, receipt, evidence: 'Latest receipt is manual or a currency check; nightly execution remains unproven.' };
|
|
273
294
|
if (receipt.state !== 'completed') return { state: 'failed', ageHours, receipt, evidence: 'Coordinated update terminal state is unverified.' };
|
|
274
295
|
if (ageHours > maxAgeHours) return { state: 'stale', ageHours, receipt, evidence: `Last coordinated update is ${ageHours.toFixed(1)}h old.` };
|
|
275
296
|
const verification = { ok: bound && receipt.mode === 'apply' && receipt.schedulerIdentity === registration.identity && receipt.state === 'completed' && receipt.ok === true && ageHours >= 0 && ageHours <= maxAgeHours };
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
// Reviewed stabilization release boundaries. Legacy tests remain developer diagnostics.
|
|
2
2
|
export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
3
3
|
"source": [
|
|
4
|
+
{
|
|
5
|
+
"id": "public-admin-release-and-engineering-status",
|
|
6
|
+
"reason": "Latest code release stays separate from corpus identity; issue and pull-request pagination preserves the complete engineering queue and failed probes remain unknown without inventing owner action",
|
|
7
|
+
"files": ["tests/unit/explainer-api.test.mjs", "tests/unit/admin-render.test.mjs"]
|
|
8
|
+
},
|
|
4
9
|
{
|
|
5
10
|
"id": "source-bound-installed-update-coordinator",
|
|
6
11
|
"reason": "Allowed channels and immutable Claude artifact paths reject downgrades, ambiguous registry records, changed bytes, extra active resources and escape paths; Console success requires the exact launched source closure. POSIX owner fixtures additionally enforce original-prefix installs, existing-tool-only policy, shared locks and source-bound scheduler receipts",
|
|
7
|
-
"files": ["tests/unit/developer-update-policy-portable.test.mjs", "tests/unit/plugin-artifact-proof.test.mjs", "tests/unit/console-suite-update.test.mjs"],
|
|
12
|
+
"files": ["tests/unit/developer-update-policy-portable.test.mjs", "tests/unit/plugin-artifact-proof.test.mjs", "tests/unit/console-suite-update.test.mjs", "tests/unit/developer-update-scheduled-receipt.test.mjs"],
|
|
8
13
|
"platformFiles": {
|
|
9
|
-
"linux": ["tests/unit/developer-update.test.mjs"],
|
|
10
|
-
"macos": ["tests/unit/developer-update.test.mjs"]
|
|
14
|
+
"linux": ["tests/unit/developer-update.test.mjs", "tests/unit/developer-update-codex-launcher.test.mjs"],
|
|
15
|
+
"macos": ["tests/unit/developer-update.test.mjs", "tests/unit/developer-update-codex-launcher.test.mjs"]
|
|
11
16
|
}
|
|
12
17
|
},
|
|
13
18
|
{
|