ruvnet-brain 4.3.10 → 4.3.13
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 +1 -1
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/nightly-scheduler.mjs +27 -2
- package/plugin/skills/release-proof/SKILL.md +6 -2
- package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
- package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
- package/scripts/nightly-package-observation.mjs +67 -0
- package/scripts/nightly-two-run-proof.mjs +82 -8
- package/scripts/public-verification-aggregate.mjs +8 -0
- package/scripts/public-verification-lane.mjs +5 -1
- package/scripts/publication-receipt.mjs +51 -8
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
|
|
|
7
7
|
|
|
8
8
|
# 🧠 RuvNet Brain
|
|
9
9
|
|
|
10
|
-
### 🧠 RuvNet Brain — [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
11
11
|
|
|
12
12
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.13",
|
|
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": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"version:set": "node scripts/set-version.mjs",
|
|
17
17
|
"qa:pr": "node scripts/qa-runner.mjs",
|
|
18
18
|
"maintenance": "node scripts/development-maintenance.mjs",
|
|
19
|
-
"qa:release": "
|
|
19
|
+
"qa:release": "npm run version:check && npm run release:authority && npm exec -- vitest run --config tests/qe/release/vitest.config.mjs --maxWorkers=1",
|
|
20
20
|
"release:authority": "node scripts/release-authority.mjs",
|
|
21
21
|
"card-from-source": "node scripts/card-from-source.mjs",
|
|
22
22
|
"release-abort-stale": "node scripts/release-abort-stale.mjs",
|
|
@@ -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 an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.13",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -310,6 +310,17 @@ const xmlDecode = value => value.replace(/&#(?:x([a-f0-9]+)|([0-9]+));|&(amp|lt|
|
|
|
310
310
|
: ({ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" })[named.toLowerCase()]);
|
|
311
311
|
const onlyXmlMatch = (xml, pattern) => { const hits = [...xml.matchAll(pattern)]; return hits.length === 1 ? hits[0][1] : null; };
|
|
312
312
|
const windowsArguments = record => `"${record.runnerPath}" --registration "${record.recordPath}"`;
|
|
313
|
+
function windowsTaskXml(record) {
|
|
314
|
+
const boundary = new Date(Date.now() + 60_000).toISOString();
|
|
315
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
316
|
+
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
317
|
+
<RegistrationInfo><Author>RuvNet Brain</Author></RegistrationInfo>
|
|
318
|
+
<Triggers><CalendarTrigger><StartBoundary>${boundary}</StartBoundary><Enabled>true</Enabled><ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay></CalendarTrigger></Triggers>
|
|
319
|
+
<Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
320
|
+
<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT1H</ExecutionTimeLimit></Settings>
|
|
321
|
+
<Actions Context="Author"><Exec><Command>${xmlEscape(record.nodePath)}</Command><Arguments>${xmlEscape(windowsArguments(record))}</Arguments></Exec></Actions>
|
|
322
|
+
</Task>`;
|
|
323
|
+
}
|
|
313
324
|
function exactWindowsCommand(xml, record) {
|
|
314
325
|
const actions = onlyXmlMatch(xml, /<Actions(?:\s[^>]*)?>([\s\S]*?)<\/Actions>/g);
|
|
315
326
|
if (actions === null || actions.replace(/<Exec(?:\s[^>]*)?>[\s\S]*?<\/Exec>/g, '').trim()) return false;
|
|
@@ -393,8 +404,22 @@ export function installScheduler(record, { platform = process.platform, env = pr
|
|
|
393
404
|
return { ok: true, artifact, already: false };
|
|
394
405
|
}
|
|
395
406
|
const taskCommand = `"${record.nodePath}" ${windowsArguments(record)}`;
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
let created;
|
|
408
|
+
if (testMode) {
|
|
409
|
+
// The fixture adapter models the legacy /TR call; keep it isolated from the machine path.
|
|
410
|
+
created = run('schtasks', ['/Create', '/SC', 'DAILY', '/TN', record.identity, '/TR', taskCommand,
|
|
411
|
+
'/ST', `${String(NIGHTLY_HOUR).padStart(2, '0')}:${String(NIGHTLY_MINUTE).padStart(2, '0')}`, '/F'], { encoding: 'utf8' });
|
|
412
|
+
} else {
|
|
413
|
+
// schtasks /TR rejects commands longer than 261 characters. Importing equivalent XML keeps the
|
|
414
|
+
// exact Node executable and argument identity while allowing long temporary/user paths.
|
|
415
|
+
const xmlPath = path.join(path.dirname(record.recordPath), `${record.identity}.task.xml`);
|
|
416
|
+
// schtasks requires a UTF-16LE BOM when importing XML; without it Windows
|
|
417
|
+
// reports the otherwise valid document as "one root element" malformed.
|
|
418
|
+
const xmlBytes = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(windowsTaskXml(record), 'utf16le')]);
|
|
419
|
+
fs.writeFileSync(xmlPath, xmlBytes, { flag: 'wx' });
|
|
420
|
+
try { created = run('schtasks', ['/Create', '/XML', xmlPath, '/TN', record.identity, '/F'], { encoding: 'utf8' }); }
|
|
421
|
+
finally { fs.rmSync(xmlPath, { force: true }); }
|
|
422
|
+
}
|
|
398
423
|
if (created.error || created.status !== 0) return { ok: false, artifact, why: created.error?.message || String(created.stderr || `exit ${created.status}`).trim() };
|
|
399
424
|
return { ok: true, artifact, already: false };
|
|
400
425
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: release-proof
|
|
3
3
|
description: Fail-closed exact-artifact release and deployment authority. Use before saying a release is ready, pushing a release commit, publishing npm packages, creating GitHub releases, deploying production, closing release-blocking issues, or claiming all gates are green. Requires clean immutable lineage, zero labeled release blockers, exact-SHA GitHub success, nonzero no-skip QE, packed-artifact host tests, installed Brain/RVF proof, and post-publication byte verification.
|
|
4
|
-
updated: 2026-09-
|
|
4
|
+
updated: 2026-09-07
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Release Proof
|
|
@@ -23,7 +23,11 @@ green.
|
|
|
23
23
|
7. Require the source package, Claude manifest, Codex manifest, packed npm version, bundle
|
|
24
24
|
`brainVersion`/`releaseTag`, and both installed host versions to identify one exact generation.
|
|
25
25
|
8. Require the active Brain registry to contain the `ruvnet-brain` RVF store and require narrow,
|
|
26
|
-
broad, and concurrent cited searches to complete within 80% of their deadline.
|
|
26
|
+
broad, and concurrent cited candidate searches to complete within 80% of their deadline.
|
|
27
|
+
For fresh public stabilization verification, `stabilization-public-deadline-v1` requires the
|
|
28
|
+
first cited search in every host mode and the Brain probe within the enforced 30-second
|
|
29
|
+
deadline. The 24-second headroom target remains a performance follow-up, not promotion authority.
|
|
30
|
+
Receipts without this named policy retain the legacy 80% rule; failed runs are not relabeled.
|
|
27
31
|
9. Publish only through the protected release workflow. Never run `npm publish` or `gh release
|
|
28
32
|
create` locally.
|
|
29
33
|
10. After publication, download npm and GitHub artifacts, compare their bytes with the seal, install
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Receipt contract
|
|
2
2
|
|
|
3
|
-
Updated: 2026-09-
|
|
3
|
+
Updated: 2026-09-07
|
|
4
4
|
|
|
5
5
|
The authority accepts only typed receipt schemas. The candidate receipts are emitted by
|
|
6
6
|
`release-candidate-preflight` and imported by `protected-release`; publication receipts are emitted
|
|
@@ -38,9 +38,38 @@ Required bindings:
|
|
|
38
38
|
and installed Claude/Codex versions
|
|
39
39
|
- npm and GitHub release bytes matching the candidate digest
|
|
40
40
|
- clean installed Claude and Codex results from the public package
|
|
41
|
-
- installed Brain self-RVF and
|
|
41
|
+
- installed Brain self-RVF and measured first cited searches within the declared acceptance policy
|
|
42
|
+
- `acceptancePolicy: stabilization-public-deadline-v1` requires a fixed 30,000ms deadline and
|
|
43
|
+
finite, nonnegative host and Brain timings at or below it; larger budgets are rejected
|
|
44
|
+
- receipts without that policy retain the legacy 80-percent margin; unknown policies fail
|
|
42
45
|
- successful exact-SHA `published-surface-probe`
|
|
43
46
|
|
|
47
|
+
Stuart authorized a materially improved stabilization release on September 7, rather than
|
|
48
|
+
requiring every historical performance goal for promotion. The public runtime already enforces
|
|
49
|
+
a 30-second deadline. The new policy removes only the additional 24-second promotion margin.
|
|
50
|
+
It does not replace first-search timings with warm timings or reduce retrieval, citation,
|
|
51
|
+
artifact-integrity, native-update, or host-installation requirements. Fresh signed evidence is
|
|
52
|
+
required; prior failed workflows remain failed.
|
|
53
|
+
|
|
54
|
+
Native update verification suppresses optional npm command shims only during the initial
|
|
55
|
+
reader installation (`npm_config_bin_links=false`). The scheduler retains npm's default
|
|
56
|
+
command-shim behavior, which `npx` requires. The receipt records this installation configuration.
|
|
57
|
+
The default reader install creates a `semver` symlink that 4.3.10's updater rejects; fixing that
|
|
58
|
+
default-install compatibility remains a follow-up. Symlink rejection is not bypassed.
|
|
59
|
+
|
|
60
|
+
The native scheduler executes its actual production target, `ruvnet-brain@latest`. A fresh isolated
|
|
61
|
+
npm cache is prepared and its package files must match the verified public tarball installation.
|
|
62
|
+
Exactly one cached Brain package is allowed. Full package tree observations before and after each
|
|
63
|
+
of two scheduled runs, plus each run's actual desired version, bind the execution evidence to the
|
|
64
|
+
published package. The consumer validates these observations and their time bounds. A changed
|
|
65
|
+
latest version or changed package bytes fails the proof. Dependencies are outside the package-tree
|
|
66
|
+
comparison. The two runs verify the already-current update path; they do not establish fallback
|
|
67
|
+
reinstallation or new upstream corpus generation. Only empty installer-created preserved
|
|
68
|
+
directories are removed in the disposable fixture; populated or linked entries are retained and
|
|
69
|
+
still fail duplicate-copy acceptance. Storage observations wait for the refresh owner to exit
|
|
70
|
+
and its lock to be removed. The second run may add only measured receipt and bounded log bytes;
|
|
71
|
+
any other managed-storage growth remains a failure.
|
|
72
|
+
|
|
44
73
|
Before provider mutation, `protected-release` revalidates the imported candidate receipt, package
|
|
45
74
|
payload, source binding, and digest against current `origin/main`. It consumes the long-lane proof;
|
|
46
75
|
it does not rerun CI, integration, UX, or stranger qualification.
|
|
@@ -169,15 +169,24 @@ export function evaluatePublicationReceipt(candidate, publication) {
|
|
|
169
169
|
const item = publication?.[surface];
|
|
170
170
|
if (item?.sha !== sha || item?.artifactSha256 !== digest) failures.push(fail('PUBLIC_ARTIFACT_MISMATCH', `${surface} differs from candidate seal`));
|
|
171
171
|
}
|
|
172
|
+
const stabilization = publication?.acceptancePolicy === 'stabilization-public-deadline-v1';
|
|
173
|
+
const deadline = Number(publication?.brain?.deadlineMs);
|
|
174
|
+
if (publication?.acceptancePolicy !== undefined && !stabilization) {
|
|
175
|
+
failures.push(fail('PUBLIC_ACCEPTANCE_POLICY', 'unknown public acceptance policy'));
|
|
176
|
+
}
|
|
177
|
+
const withinDeadline = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
178
|
+
&& Number.isFinite(deadline) && deadline > 0
|
|
179
|
+
&& (!stabilization || deadline === 30_000)
|
|
180
|
+
&& value <= deadline * (stabilization ? 1 : 0.8);
|
|
172
181
|
for (const hostName of ['claudeOnly', 'codexOnly', 'dual']) {
|
|
173
182
|
const host = publication?.installed?.[hostName];
|
|
174
183
|
if (host?.status !== 'PASS' || host?.doctorExit !== 0 || host?.artifactSha256 !== digest
|
|
175
|
-
|| host?.functionalSearch !== true || !(host?.searchMs
|
|
184
|
+
|| host?.functionalSearch !== true || !withinDeadline(host?.searchMs)) {
|
|
176
185
|
failures.push(fail('PUBLIC_HOST_NOT_PASS', `${hostName} is not running a doctor-clean functional sealed public artifact`));
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
188
|
const brain = publication?.brain || {};
|
|
180
|
-
if (brain.status !== 'PASS' || brain.selfStore !== true || !(brain.broadMs
|
|
189
|
+
if (brain.status !== 'PASS' || brain.selfStore !== true || !withinDeadline(brain.broadMs)) failures.push(fail('PUBLIC_BRAIN_NOT_PASS', 'public installed Brain acceptance failed'));
|
|
181
190
|
const probes = Array.isArray(publication?.postPublicationChecks) ? publication.postPublicationChecks : [];
|
|
182
191
|
const probe = probes.find((check) => check?.name === 'published-surface-probe' && check?.sha === sha);
|
|
183
192
|
if (probe?.status !== 'completed' || probe?.conclusion !== 'success') failures.push(fail('POST_PUBLICATION_CHECK_NOT_GREEN', 'published-surface-probe is not green'));
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
6
|
+
|
|
7
|
+
// npm dependencies are separately installed outside the immutable package payload.
|
|
8
|
+
export function packageTreeObservation(root) {
|
|
9
|
+
const files = [];
|
|
10
|
+
const visit = (dir, prefix = '') => {
|
|
11
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
12
|
+
if (!prefix && name === 'node_modules') continue;
|
|
13
|
+
const file = path.join(dir, name);
|
|
14
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
15
|
+
const stat = fs.lstatSync(file);
|
|
16
|
+
if (stat.isDirectory()) visit(file, relative);
|
|
17
|
+
else if (stat.isFile()) files.push({ path: relative, sha256: hash(fs.readFileSync(file)) });
|
|
18
|
+
else throw new Error(`npm package contains an unsupported entry: ${relative}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
visit(root);
|
|
22
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
23
|
+
return { name: manifest.name, version: manifest.version, fileCount: files.length,
|
|
24
|
+
treeSha256: hash(JSON.stringify(files)) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function observeNpxPackage(npmCache, expected) {
|
|
28
|
+
const base = path.join(npmCache, '_npx');
|
|
29
|
+
const roots = fs.existsSync(base) ? fs.readdirSync(base).map((name) =>
|
|
30
|
+
path.join(base, name, 'node_modules', 'ruvnet-brain')).filter((root) => fs.existsSync(root)) : [];
|
|
31
|
+
if (roots.length !== 1) throw new Error(`expected exactly one isolated npx Brain package, found ${roots.length}`);
|
|
32
|
+
const observed = packageTreeObservation(roots[0]);
|
|
33
|
+
if (JSON.stringify(observed) !== JSON.stringify(expected)) throw new Error('npx package bytes differ from verified public package');
|
|
34
|
+
return { ...observed, path: roots[0], observedAt: new Date().toISOString() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateNpxObservations(proof) {
|
|
38
|
+
const expected = proof.packageExecution?.expected;
|
|
39
|
+
const observations = proof.packageExecution?.observations;
|
|
40
|
+
if (proof.packageExecution?.policy !== 'production-latest-exact-cache-v1'
|
|
41
|
+
|| proof.registration?.packageTarget?.spec !== 'ruvnet-brain@latest'
|
|
42
|
+
|| proof.registration.packageTarget.sha256 !== null
|
|
43
|
+
|| expected?.packageSha256 !== proof.candidate?.sha256
|
|
44
|
+
|| expected?.name !== 'ruvnet-brain' || expected.version !== proof.candidate?.version
|
|
45
|
+
|| !/^[a-f0-9]{64}$/.test(expected.treeSha256 || '') || !Number.isSafeInteger(expected.fileCount)
|
|
46
|
+
|| expected.fileCount <= 0 || !Array.isArray(observations) || observations.length !== 3) {
|
|
47
|
+
return ['native production package execution evidence is incomplete'];
|
|
48
|
+
}
|
|
49
|
+
const failures = [];
|
|
50
|
+
for (const observed of observations) {
|
|
51
|
+
if (observed.name !== expected.name || observed.version !== expected.version
|
|
52
|
+
|| observed.treeSha256 !== expected.treeSha256 || observed.fileCount !== expected.fileCount
|
|
53
|
+
|| typeof observed.path !== 'string' || !observed.path
|
|
54
|
+
|| !Number.isFinite(Date.parse(observed.observedAt))) failures.push('native npx package identity differs');
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(proof.runs) || proof.runs.length !== 2) return [...failures, 'native package run evidence is missing'];
|
|
57
|
+
for (let i = 0; i < 2; i++) {
|
|
58
|
+
const run = proof.runs[i].receipt;
|
|
59
|
+
if (run?.desiredVersion !== expected.version
|
|
60
|
+
|| Date.parse(observations[i].observedAt) > Date.parse(run?.startedAt)
|
|
61
|
+
|| Date.parse(observations[i + 1].observedAt) < Date.parse(run?.finishedAt)
|
|
62
|
+
|| Date.parse(observations[i + 1].observedAt) > Date.parse(proof.observedAt)) {
|
|
63
|
+
failures.push('native package observation does not bound the exact-version run');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return failures;
|
|
67
|
+
}
|
|
@@ -8,8 +8,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
9
|
import { assessLifecycleEvidence } from '../kb/lifecycle-evidence-retention.mjs';
|
|
10
10
|
import { managedStorageInventory } from '../kb/update-storage-transaction.mjs';
|
|
11
|
-
import { REQUIRED_REFRESH_PHASES, validateCurrentRunPhaseExecution } from '../kb/refresh-run.mjs';
|
|
11
|
+
import { REQUIRED_REFRESH_PHASES, validateCurrentRunPhaseExecution, inspectRefreshOwner, refreshLockPath } from '../kb/refresh-run.mjs';
|
|
12
12
|
import { npmInvocation } from './npm-invocation.mjs';
|
|
13
|
+
import { packageTreeObservation, observeNpxPackage, validateNpxObservations } from './nightly-package-observation.mjs';
|
|
13
14
|
import { validateRefreshReceiptEnvelope } from '../plugin/scripts/nightly-scheduler.mjs';
|
|
14
15
|
import { validateCoverageDirectory, validateCoverageLedger } from '../plugin/scripts/coverage-integrity.mjs';
|
|
15
16
|
|
|
@@ -67,6 +68,29 @@ export function stageExactBundle({ bundlePath, packageRoot } = {}) {
|
|
|
67
68
|
return { sourcePath, stagedPath, bytes: bytes.length, sha256: sha256(bytes) };
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
export function removeEmptyInstallerScaffolds(kbDir) {
|
|
72
|
+
const parent = path.dirname(kbDir);
|
|
73
|
+
const prefix = `${path.basename(kbDir)}.install-preserved-`;
|
|
74
|
+
const removed = [];
|
|
75
|
+
for (const name of fs.readdirSync(parent)) {
|
|
76
|
+
if (!name.startsWith(prefix)) continue;
|
|
77
|
+
const file = path.join(parent, name);
|
|
78
|
+
const stat = fs.lstatSync(file);
|
|
79
|
+
if (stat.isDirectory() && !stat.isSymbolicLink() && fs.readdirSync(file).length === 0) {
|
|
80
|
+
fs.rmdirSync(file); removed.push(file);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return removed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function observeUpdateLog(file) {
|
|
87
|
+
if (!fs.existsSync(file)) return { bytes: 0, sha256: sha256(Buffer.alloc(0)) };
|
|
88
|
+
const stat = fs.lstatSync(file);
|
|
89
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('native update log is not a regular file');
|
|
90
|
+
const bytes = fs.readFileSync(file);
|
|
91
|
+
return { bytes: bytes.length, sha256: sha256(bytes) };
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
function nightlyReceipts(brainHome, identity) {
|
|
71
95
|
const dir = path.join(brainHome, 'refresh-runs');
|
|
72
96
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -80,10 +104,18 @@ function nightlyReceipts(brainHome, identity) {
|
|
|
80
104
|
&& receipt.action === 'nightly' && receipt.schedulerIdentity === identity);
|
|
81
105
|
}
|
|
82
106
|
|
|
107
|
+
function refreshExecutionStopped(receipt, kbDir) {
|
|
108
|
+
if (inspectRefreshOwner(receipt.ownerToken) !== 'dead') return false;
|
|
109
|
+
const lock = refreshLockPath(kbDir);
|
|
110
|
+
const parent = path.dirname(lock);
|
|
111
|
+
return !fs.existsSync(lock) && (!fs.existsSync(parent) || !fs.readdirSync(parent)
|
|
112
|
+
.some((name) => name.startsWith(`${path.basename(lock)}.release-`)));
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
// Dependency seams here exercise orchestration without touching a developer's native scheduler.
|
|
84
116
|
// Only runNightlyTwoRunProof writes proof, and it supplies the real installed adapter and commands.
|
|
85
117
|
export async function triggerNativeRun({ scheduler, registration, platform, env, brainHome, kbDir,
|
|
86
|
-
timeoutMs, command = run, receipts = nightlyReceipts, pause = sleep, now = Date.now }) {
|
|
118
|
+
timeoutMs, command = run, receipts = nightlyReceipts, pause = sleep, now = Date.now, stopped = refreshExecutionStopped }) {
|
|
87
119
|
const identity = registration.identity;
|
|
88
120
|
if (!/^com\.ruvnet\.brain-update\.proof-[A-Za-z0-9._-]+$/.test(identity || '')) {
|
|
89
121
|
throw new Error('native proof requires a unique proof scheduler identity');
|
|
@@ -110,7 +142,13 @@ export async function triggerNativeRun({ scheduler, registration, platform, env,
|
|
|
110
142
|
if (!installed.ok) throw new Error(`native scheduler unavailable: ${installed.why}`);
|
|
111
143
|
const status = scheduler.schedulerStatus(options);
|
|
112
144
|
if (status.state !== 'on') throw new Error(`proof scheduler is not on: ${status.evidence}`);
|
|
113
|
-
if (platform === 'darwin')
|
|
145
|
+
if (platform === 'darwin') {
|
|
146
|
+
// Darwin has getuid, but the qualification suite also exercises the Darwin
|
|
147
|
+
// branch from Windows. Keep that simulation portable without changing the
|
|
148
|
+
// real launchctl identity on macOS.
|
|
149
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
150
|
+
command('launchctl', ['kickstart', '-k', `gui/${uid}/${identity}`]);
|
|
151
|
+
}
|
|
114
152
|
if (platform === 'win32') command('schtasks', ['/Run', '/TN', identity]);
|
|
115
153
|
// Cron itself must deliver the Linux trigger. Never execute its command directly as proof.
|
|
116
154
|
const startedAt = now();
|
|
@@ -124,8 +162,12 @@ export async function triggerNativeRun({ scheduler, registration, platform, env,
|
|
|
124
162
|
// Stop the minute cadence at the first receipt, including RUNNING, so long updates do
|
|
125
163
|
// not launch again. Removing a cron row does not kill its already running child.
|
|
126
164
|
if (platform === 'linux' && !removed) remove();
|
|
127
|
-
if (['SUCCEEDED', 'FAILED', 'ABANDONED'].includes(created[0].receipt.status)
|
|
165
|
+
if (['SUCCEEDED', 'FAILED', 'ABANDONED'].includes(created[0].receipt.status)
|
|
166
|
+
&& stopped(created[0].receipt, kbDir)) {
|
|
167
|
+
await pause(250); // Let the exited child's npm parent finish flushing its log.
|
|
168
|
+
return { ...created[0],
|
|
128
169
|
trigger: { kind: { darwin: 'launchctl-kickstart', linux: 'cron-tick', win32: 'schtasks-run' }[platform], identity } };
|
|
170
|
+
}
|
|
129
171
|
}
|
|
130
172
|
if (platform === 'linux' && !observedStart && now() - startedAt >= 120_000) {
|
|
131
173
|
throw new Error('cron did not start the owned proof job within two minutes; check that the cron service is running');
|
|
@@ -170,8 +212,17 @@ function validateStorageEvidence({ first, second, inventoryBefore, inventoryAfte
|
|
|
170
212
|
if (inventory.additionalFullCorpusCopyCount !== 0) failures.push(`${label} inventory has redundant corpus copies`);
|
|
171
213
|
if (!Number.isSafeInteger(inventory.totalManagedBytes) || inventory.totalManagedBytes < 0) failures.push(`${label} inventory has no measured byte count`);
|
|
172
214
|
}
|
|
215
|
+
let logGrowth = 0;
|
|
216
|
+
if (retention.updateLog !== undefined) {
|
|
217
|
+
const { before, after } = retention.updateLog;
|
|
218
|
+
if (![before, after].every((row) => Number.isSafeInteger(row?.bytes) && row.bytes >= 0
|
|
219
|
+
&& /^[a-f0-9]{64}$/.test(row.sha256 || '')) || !Number.isSafeInteger(retention.policy?.maxEvidenceBytes)
|
|
220
|
+
|| retention.policy.maxEvidenceBytes <= 0 || after.bytes > retention.policy.maxEvidenceBytes) {
|
|
221
|
+
failures.push('native log byte measurements are invalid or exceed the evidence budget');
|
|
222
|
+
} else logGrowth = Math.max(0, after.bytes - before.bytes);
|
|
223
|
+
}
|
|
173
224
|
if (inventoryAfterSecond.totalManagedBytes > inventoryAfterFirst.totalManagedBytes
|
|
174
|
-
+ Math.max(0, retention.after?.bytes - retention.before?.bytes)) {
|
|
225
|
+
+ Math.max(0, retention.after?.bytes - retention.before?.bytes) + logGrowth) {
|
|
175
226
|
failures.push('second no-op increased managed storage outside retained lifecycle evidence');
|
|
176
227
|
}
|
|
177
228
|
if (retention.withinBudget !== true || retention.unsafe?.length) failures.push('lifecycle evidence is not within policy');
|
|
@@ -253,6 +304,9 @@ export function validateNightlyProofReceipt(receipt, { platform, version, packag
|
|
|
253
304
|
if (!/^[a-f0-9]{40}$/.test(sourceSha || '') || receipt.sourceSha !== sourceSha) failures.push('native proof source SHA differs');
|
|
254
305
|
if (!/^\d+$/.test(String(workflowRunId || '')) || String(receipt.workflowRunId || '') !== String(workflowRunId)) failures.push('native proof workflow run differs');
|
|
255
306
|
if (!/^com\.ruvnet\.brain-update\.proof-[A-Za-z0-9._-]+$/.test(receipt.identity || '')) failures.push('native proof identity is not isolated');
|
|
307
|
+
if (receipt.packageExecution !== undefined || receipt.registration?.packageTarget?.spec === 'ruvnet-brain@latest') {
|
|
308
|
+
failures.push(...validateNpxObservations(receipt));
|
|
309
|
+
}
|
|
256
310
|
const observedAt = Date.parse(receipt.observedAt);
|
|
257
311
|
if (!Number.isFinite(observedAt) || observedAt > Date.now() + 60_000) failures.push('native proof observation time is invalid');
|
|
258
312
|
if (!Array.isArray(receipt.runs) || receipt.runs.length !== 2) failures.push('native proof requires exactly two runs');
|
|
@@ -330,14 +384,23 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
|
|
|
330
384
|
run(npm.executable, npm.args, { env });
|
|
331
385
|
const packageRoot = path.join(prefix, 'node_modules', 'ruvnet-brain');
|
|
332
386
|
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
387
|
+
const expectedPackage = packageTreeObservation(packageRoot);
|
|
388
|
+
// Exercise the production target, not npx's ambiguous positional local-tarball path.
|
|
389
|
+
const prepare = npmInvocation(['exec', '--yes', '--package=ruvnet-brain@latest', '--', 'ruvnet-brain', '--help']);
|
|
390
|
+
run(prepare.executable, prepare.args, { env, cwd: home });
|
|
391
|
+
const packageObservations = [observeNpxPackage(npmCache, expectedPackage)];
|
|
392
|
+
// Register the exact public package's plugin in this fresh home. GitHub shorthand can
|
|
393
|
+
// select SSH on Windows and leave no staged plugin for the first scheduled activation.
|
|
394
|
+
env.RUVNET_CLAUDE_MARKETPLACE_SOURCE = packageRoot;
|
|
333
395
|
const stagedBundle = stageExactBundle({ bundlePath: bundle, packageRoot });
|
|
334
396
|
run(process.execPath, [path.join(packageRoot, 'bin', 'install.mjs'), '--yes', '--no-nightly-prompt'], {
|
|
335
|
-
env, stdio: 'inherit', encoding: undefined,
|
|
397
|
+
env: { ...env, npm_config_bin_links: 'false' }, stdio: 'inherit', encoding: undefined,
|
|
336
398
|
});
|
|
399
|
+
const removedEmptyScaffolds = removeEmptyInstallerScaffolds(kbDir);
|
|
337
400
|
scheduler = await import(`${pathToFileURL(path.join(packageRoot, 'plugin', 'scripts', 'nightly-scheduler.mjs')).href}?proof=${Date.now()}`);
|
|
338
401
|
registration = scheduler.installNightlyRunner({ brainHome,
|
|
339
402
|
source: path.join(packageRoot, 'bin', 'nightly-refresh.mjs'), nodePath: process.execPath, identity, env,
|
|
340
|
-
packageTarget: { spec:
|
|
403
|
+
packageTarget: { spec: 'ruvnet-brain@latest', sha256: null },
|
|
341
404
|
bundleTarget: { spec: stagedBundle.sourcePath, sha256: stagedBundle.sha256 } });
|
|
342
405
|
const inventoryBefore = managedStorageInventory(kbDir);
|
|
343
406
|
const kick = () => triggerNativeRun({ scheduler, registration, platform: process.platform,
|
|
@@ -351,14 +414,25 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
|
|
|
351
414
|
const firstEntry = await kick();
|
|
352
415
|
firstEntry.installedCoverage = observeCoverage();
|
|
353
416
|
const inventoryAfterFirst = managedStorageInventory(kbDir);
|
|
417
|
+
packageObservations.push(observeNpxPackage(npmCache, expectedPackage));
|
|
418
|
+
const retentionAfterFirst = assessLifecycleEvidence({ brainHome, kbDir });
|
|
419
|
+
const logAfterFirst = observeUpdateLog(logPath);
|
|
354
420
|
const secondEntry = await kick();
|
|
355
421
|
secondEntry.installedCoverage = observeCoverage();
|
|
356
422
|
const inventoryAfterSecond = managedStorageInventory(kbDir);
|
|
357
|
-
|
|
423
|
+
packageObservations.push(observeNpxPackage(npmCache, expectedPackage));
|
|
424
|
+
const retentionAfterSecond = assessLifecycleEvidence({ brainHome, kbDir });
|
|
425
|
+
const retention = { ...retentionAfterSecond, before: retentionAfterFirst.after,
|
|
426
|
+
withinBudget: retentionAfterFirst.withinBudget && retentionAfterSecond.withinBudget,
|
|
427
|
+
unsafe: [...retentionAfterFirst.unsafe, ...retentionAfterSecond.unsafe],
|
|
428
|
+
updateLog: { before: logAfterFirst, after: observeUpdateLog(logPath) } };
|
|
358
429
|
const receipt = {
|
|
359
430
|
schemaVersion: 1,
|
|
360
431
|
kind: 'ruvnet-brain-native-two-run-nightly-proof',
|
|
361
432
|
scope: 'installed-update',
|
|
433
|
+
installationConfiguration: { npmBinLinks: false, source: 'initial-reader-install-only', removedEmptyScaffolds },
|
|
434
|
+
packageExecution: { policy: 'production-latest-exact-cache-v1',
|
|
435
|
+
expected: { ...expectedPackage, packageSha256 }, observations: packageObservations },
|
|
362
436
|
upstreamFreshness: 'UNKNOWN',
|
|
363
437
|
observedAt: new Date().toISOString(),
|
|
364
438
|
platform: process.platform,
|
|
@@ -37,6 +37,14 @@ export function validatePublicVerificationLeaf(leaf, { publicKey } = {}) {
|
|
|
37
37
|
if (leaf.publicBytes?.npmExact !== true || leaf.publicBytes?.githubExact !== true
|
|
38
38
|
|| leaf.publicBytes?.bundleExact !== true || leaf.installed?.version !== leaf.version
|
|
39
39
|
|| leaf.installed?.loaderVerified !== true) throw new Error(`${leaf.os}/${leaf.mode} public bytes or loader are unverified`);
|
|
40
|
+
if (leaf.acceptancePolicy !== undefined || leaf.searchTiming !== undefined) {
|
|
41
|
+
const timing = leaf.searchTiming;
|
|
42
|
+
if (leaf.acceptancePolicy !== 'stabilization-public-deadline-v1' || timing?.deadlineMs !== 30_000
|
|
43
|
+
|| ![timing.firstSearchMs, timing.broadMs].every((value) => typeof value === 'number'
|
|
44
|
+
&& Number.isFinite(value) && value >= 0 && value <= timing.deadlineMs)) {
|
|
45
|
+
throw new Error(`${leaf.os}/${leaf.mode} public search acceptance policy or timing is invalid`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
40
48
|
if (leaf.coverage?.verified !== true || leaf.coverage.eligibleCurrent !== leaf.coverage.eligibleTotal
|
|
41
49
|
|| leaf.coverage.gistCurrent !== leaf.coverage.gistTotal) throw new Error(`${leaf.os}/${leaf.mode} coverage is incomplete`);
|
|
42
50
|
if (!Array.isArray(leaf.untested) || leaf.untested.length || leaf.skipped !== 0 || leaf.unknown !== 0) {
|
|
@@ -178,13 +178,17 @@ export async function createPublicVerificationLane({
|
|
|
178
178
|
failures.push({ mode, reason: error.message, retrieval });
|
|
179
179
|
continue;
|
|
180
180
|
}
|
|
181
|
+
// Native scheduler lifecycle is qualified in the reviewed cross-platform
|
|
182
|
+
// release suite. The live public adapter opts out because that proof relies
|
|
183
|
+
// on maintainer-only host tooling; test adapters still exercise the seam.
|
|
181
184
|
let nativeNightly;
|
|
182
|
-
if (mode === 'dual') {
|
|
185
|
+
if (mode === 'dual' && adapter.publicNativeNightly !== false) {
|
|
183
186
|
if (typeof adapter.runNativeNightly !== 'function') throw new Error('native nightly proof adapter is required');
|
|
184
187
|
nativeNightly = await adapter.runNativeNightly({ identity, workflowRunId: String(workflowRunId) });
|
|
185
188
|
}
|
|
186
189
|
leaves.push(createPublicVerificationLeaf({
|
|
187
190
|
...common,
|
|
191
|
+
...(publication.acceptancePolicy ? { acceptancePolicy: publication.acceptancePolicy, searchTiming: { firstSearchMs: installed.searchMs, broadMs: publication.brain.broadMs, deadlineMs: publication.brain.deadlineMs } } : {}),
|
|
188
192
|
...(nativeNightly === undefined ? {} : { nativeNightly }),
|
|
189
193
|
os,
|
|
190
194
|
mode,
|
|
@@ -7,6 +7,7 @@ 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 { createRequire } from 'node:module';
|
|
10
11
|
import { spawn, spawnSync } from 'node:child_process';
|
|
11
12
|
// ONE doctor rule and ONE mode vocabulary, shared with the staged-side check in
|
|
12
13
|
// scripts/staged-host-verifier.mjs. This file kept its own copies, spelled claudeOnly/
|
|
@@ -14,7 +15,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
14
15
|
// `fixtures.claude` described the same fixture and nothing could tell. The richer
|
|
15
16
|
// post-publication proofs below (payload assertions, MCP wiring, SOURCE.json, rpcSearch)
|
|
16
17
|
// stay here — they are this side's job, not duplication.
|
|
17
|
-
import { HOST_MODES, RECEIPT_MODE_NAMES, MODE_FROM_RECEIPT_NAME, classifyDoctor, VARIANTS } from './host-install-matrix.mjs';
|
|
18
|
+
import { HOST_MODES, RECEIPT_MODE_NAMES, MODE_FROM_RECEIPT_NAME, classifyDoctor, VARIANTS, createInstalledMcpSession } from './host-install-matrix.mjs';
|
|
18
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
20
|
import { evaluateCandidateReceipt, evaluatePublicationReceipt } from './release-proof.mjs';
|
|
20
21
|
import { verifyPayload } from './release-payload.mjs';
|
|
@@ -30,6 +31,11 @@ const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file
|
|
|
30
31
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
31
32
|
const receiptDigest = (candidate) => String(candidate?.artifact?.sha256 || '').replace(/^sha256:/, '');
|
|
32
33
|
export function commandInvocation(name, args, { platform = process.platform, env = process.env } = {}) {
|
|
34
|
+
// Git Bash's GNU tar cannot consume native drive-letter extraction destinations.
|
|
35
|
+
// Select the Windows system tar explicitly so PATH order cannot change path semantics.
|
|
36
|
+
if (platform === 'win32' && name === 'tar') return {
|
|
37
|
+
executable: path.win32.join(env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe'), args,
|
|
38
|
+
};
|
|
33
39
|
if (platform === 'win32' && /^(?:npm|npx)$/i.test(name)) return {
|
|
34
40
|
executable: env.ComSpec || 'cmd.exe', args: ['/d', '/c', `${name}.cmd`, ...args],
|
|
35
41
|
};
|
|
@@ -79,12 +85,24 @@ export function validateCandidateSource(root, { sha, version }) {
|
|
|
79
85
|
return candidateRoot;
|
|
80
86
|
}
|
|
81
87
|
function locate(name, { platform = process.platform } = {}) {
|
|
82
|
-
const query = platform === 'win32' ? (
|
|
88
|
+
const query = platform === 'win32' ? (['node', 'git'].includes(name) ? `${name}.exe` : `${name}.cmd`) : name;
|
|
83
89
|
try {
|
|
84
90
|
return command(platform === 'win32' ? 'where.exe' : 'which', [query], { platform })
|
|
85
91
|
.split(/\r?\n/).find(Boolean) || null;
|
|
86
92
|
} catch { return null; }
|
|
87
93
|
}
|
|
94
|
+
export function nativeCodexExecutable({ platform = process.platform, arch = process.arch,
|
|
95
|
+
wrapper = platform === 'win32' ? locate('codex') : null } = {}) {
|
|
96
|
+
if (platform !== 'win32') return null;
|
|
97
|
+
if (!wrapper || !['x64', 'arm64'].includes(arch)) throw new Error('native Windows Codex CLI unavailable');
|
|
98
|
+
const require = createRequire(import.meta.url);
|
|
99
|
+
const packageFile = require.resolve('@openai/codex/package.json', { paths: [path.dirname(wrapper)] });
|
|
100
|
+
const platformFile = require.resolve(`@openai/codex-win32-${arch}/package.json`, { paths: [path.dirname(packageFile)] });
|
|
101
|
+
const target = arch === 'x64' ? 'x86_64' : 'aarch64';
|
|
102
|
+
const executable = path.join(path.dirname(platformFile), 'vendor', `${target}-pc-windows-msvc`, 'bin', 'codex.exe');
|
|
103
|
+
if (!fs.statSync(executable).isFile()) throw new Error('native Windows Codex executable missing');
|
|
104
|
+
return executable;
|
|
105
|
+
}
|
|
88
106
|
export function createIsolatedPath(mode, temp, { platform = process.platform, env = process.env,
|
|
89
107
|
resolve = (name) => locate(name, { platform }) } = {}) {
|
|
90
108
|
const bin = path.join(temp, `bin-${mode}`);
|
|
@@ -103,7 +121,10 @@ export function createIsolatedPath(mode, temp, { platform = process.platform, en
|
|
|
103
121
|
if (platform !== 'win32') return `${bin}:/usr/bin:/bin`;
|
|
104
122
|
const node = resolve('node');
|
|
105
123
|
if (!node) throw new Error(`node CLI unavailable for ${mode} public host fixture`);
|
|
124
|
+
const git = resolve('git');
|
|
125
|
+
if (!git) throw new Error(`git CLI unavailable for ${mode} public host fixture`);
|
|
106
126
|
return [bin, path.dirname(node), path.join(env.SystemRoot || 'C:\\Windows', 'System32'),
|
|
127
|
+
path.dirname(git),
|
|
107
128
|
path.join(env.ProgramFiles || 'C:\\Program Files', 'Git', 'usr', 'bin')].join(path.win32.delimiter);
|
|
108
129
|
}
|
|
109
130
|
export function stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot }) {
|
|
@@ -205,6 +226,9 @@ export function rpcSearch(server, env, query, k = 5, timeoutMs = DEADLINE_MS, {
|
|
|
205
226
|
if (!Number.isSafeInteger(k) || k < 1 || k > 50) throw new Error(`invalid search result count: ${k}`);
|
|
206
227
|
return new Promise((resolve, reject) => {
|
|
207
228
|
const child = spawn(process.execPath, [server], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
229
|
+
// Preserve multibyte characters across pipe chunks before checking source-content hashes.
|
|
230
|
+
child.stdout.setEncoding('utf8');
|
|
231
|
+
child.stderr.setEncoding('utf8');
|
|
208
232
|
let stdout = '';
|
|
209
233
|
let stderr = '';
|
|
210
234
|
let timer;
|
|
@@ -275,9 +299,11 @@ export function rpcSearch(server, env, query, k = 5, timeoutMs = DEADLINE_MS, {
|
|
|
275
299
|
|
|
276
300
|
export function livePublicationAdapter({ root = process.cwd(), candidateRoot = root } = {}) {
|
|
277
301
|
const installContexts = new Map();
|
|
302
|
+
const mcpSessions = new Map();
|
|
278
303
|
const passageFileDigests = new Map();
|
|
279
304
|
let installTemp = null;
|
|
280
305
|
return {
|
|
306
|
+
publicNativeNightly: false,
|
|
281
307
|
async downloadNpm({ version, destination }) {
|
|
282
308
|
const metadata = JSON.parse(command('npm', ['view', `${PACKAGE}@${version}`, '--json']));
|
|
283
309
|
if (metadata.version !== version || !metadata.dist?.tarball) {
|
|
@@ -344,6 +370,7 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
344
370
|
// Derived from HOST_MODES, so a fourth host shape is added in ONE place and this loop
|
|
345
371
|
// cannot fall behind the staged-side check the way it did.
|
|
346
372
|
for (const mode of HOST_MODES.map((m) => RECEIPT_MODE_NAMES[m])) {
|
|
373
|
+
console.log(`Public verification: installing ${mode} on ${process.platform}`);
|
|
347
374
|
const home = path.join(temp, `home-${mode}`);
|
|
348
375
|
const codexHome = path.join(home, '.codex');
|
|
349
376
|
const brainHome = path.join(home, '.cache', 'ruvnet-brain');
|
|
@@ -367,13 +394,20 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
367
394
|
RUVNET_STRICT_INSTALL: '0',
|
|
368
395
|
CI: 'true',
|
|
369
396
|
PATH: createIsolatedPath(mode, temp),
|
|
397
|
+
...(process.platform === 'win32' && mode !== 'claudeOnly'
|
|
398
|
+
? { CODEX_BIN: nativeCodexExecutable() } : {}),
|
|
370
399
|
};
|
|
400
|
+
if (mode !== 'claudeOnly') {
|
|
401
|
+
command(env.CODEX_BIN || 'codex', ['--version'], { env, timeout: 30_000, stdio: 'inherit' });
|
|
402
|
+
command(env.CODEX_BIN || 'codex', ['plugin', 'list', '--json'], { env, timeout: 30_000, stdio: 'inherit' });
|
|
403
|
+
}
|
|
404
|
+
if (process.platform === 'win32') command('git', ['--version'], { env, timeout: 30_000, stdio: 'inherit' });
|
|
371
405
|
const installer = path.join(packageRoot, 'bin', 'install.mjs');
|
|
372
406
|
command(process.execPath, [
|
|
373
407
|
installer, '--yes', '--force', '--version', `v${version}`,
|
|
374
408
|
'--no-nightly-prompt', '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline',
|
|
375
409
|
'--no-selfcheck', '--no-verify',
|
|
376
|
-
], { env, cwd: packageRoot, timeout: 1_200_000,
|
|
410
|
+
], { env, cwd: packageRoot, timeout: 1_200_000, stdio: 'inherit' });
|
|
377
411
|
|
|
378
412
|
const verified = {};
|
|
379
413
|
if (mode !== 'codexOnly') {
|
|
@@ -394,7 +428,7 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
394
428
|
const searched = await rpcSearch(findMcpServer(home), env,
|
|
395
429
|
'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
|
|
396
430
|
command(process.execPath, [installer, '--doctor', '--hooks'], {
|
|
397
|
-
env, cwd: packageRoot, timeout: 300_000,
|
|
431
|
+
env, cwd: packageRoot, timeout: 300_000, stdio: 'inherit',
|
|
398
432
|
});
|
|
399
433
|
results[mode] = {
|
|
400
434
|
status: 'PASS', doctorExit: 0, version, artifactSha256,
|
|
@@ -426,9 +460,15 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
426
460
|
async searchInstalled({ mode, query, k }) {
|
|
427
461
|
const context = installContexts.get(mode);
|
|
428
462
|
if (!context) throw new Error(`${mode} public host is not installed`);
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
463
|
+
// Keep one MCP worker per installed host. Starting a fresh worker for every canary reloads
|
|
464
|
+
// the local embedding model repeatedly and can exceed the fixed 30-second public deadline
|
|
465
|
+
// on macOS, even when the installed Brain itself is healthy.
|
|
466
|
+
let session = mcpSessions.get(mode);
|
|
467
|
+
if (!session) {
|
|
468
|
+
session = createInstalledMcpSession({ serverPath: findMcpServer(context.home), env: context.env, timeout: DEADLINE_MS });
|
|
469
|
+
mcpSessions.set(mode, session);
|
|
470
|
+
}
|
|
471
|
+
const result = await session.search({ query, k });
|
|
432
472
|
return parseRetrievalResult(result.mcpResult, { query, k });
|
|
433
473
|
},
|
|
434
474
|
|
|
@@ -453,6 +493,8 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
453
493
|
},
|
|
454
494
|
|
|
455
495
|
async dispose() {
|
|
496
|
+
await Promise.all([...mcpSessions.values()].map((session) => session.close()));
|
|
497
|
+
mcpSessions.clear();
|
|
456
498
|
if (installTemp) fs.rmSync(installTemp, { recursive: true, force: true });
|
|
457
499
|
installTemp = null;
|
|
458
500
|
installContexts.clear();
|
|
@@ -513,6 +555,7 @@ export async function generatePublicationReceipt({
|
|
|
513
555
|
const publication = {
|
|
514
556
|
schemaVersion: 2,
|
|
515
557
|
phase: 'publication',
|
|
558
|
+
acceptancePolicy: 'stabilization-public-deadline-v1',
|
|
516
559
|
sha: candidate.sha,
|
|
517
560
|
artifactSha256: digest,
|
|
518
561
|
version: candidate.version,
|
|
@@ -532,7 +575,7 @@ export async function generatePublicationReceipt({
|
|
|
532
575
|
postPublicationChecks: [surface],
|
|
533
576
|
};
|
|
534
577
|
const result = evaluatePublicationReceipt(candidate, publication);
|
|
535
|
-
if (result.verdict !== 'PASS') throw new Error(`publication seal failed: ${
|
|
578
|
+
if (result.verdict !== 'PASS') throw new Error(`publication seal failed: ${JSON.stringify({ failures: result.failures, installed: publication.installed, brain: publication.brain })}`);
|
|
536
579
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
537
580
|
fs.writeFileSync(outPath, `${JSON.stringify(publication, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
538
581
|
return result;
|