ruvnet-brain 4.3.14 → 4.3.16
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/bin/install.mjs +51 -1
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/server.mjs +1 -1
- package/plugin/skills/ruvnet-brain/SKILL.md +5 -0
- package/scripts/dream-issue-gate.mjs +115 -0
- package/scripts/dual-host-deliberation.mjs +63 -29
- package/scripts/nightly-package-observation.mjs +9 -3
- package/scripts/nightly-two-run-proof.mjs +132 -4
- package/scripts/public-verification-aggregate.mjs +16 -7
- package/scripts/public-verification-lane.mjs +14 -6
- package/scripts/publication-receipt.mjs +127 -17
- package/scripts/qe/agentic-qe-4.3.mjs +11 -0
- package/scripts/release-transaction-provider.mjs +6 -0
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/bin/install.mjs
CHANGED
|
@@ -1722,8 +1722,27 @@ export function wireCodexPlugin({
|
|
|
1722
1722
|
}
|
|
1723
1723
|
if (announce) {
|
|
1724
1724
|
ok(`Codex Brain plugin installed and enabled (${after.version || 'version unknown'}).`);
|
|
1725
|
+
warn('Existing Codex app-server sessions may retain the previous plugin path; restart Codex before using the updated plugin.');
|
|
1725
1726
|
}
|
|
1726
|
-
return {
|
|
1727
|
+
return {
|
|
1728
|
+
host: true,
|
|
1729
|
+
action: before.installed ? 'updated' : 'installed',
|
|
1730
|
+
...after,
|
|
1731
|
+
// Codex currently exposes no generation/session lease API. This explicit guard prevents the
|
|
1732
|
+
// installer from implying that a native cache update is safe for an already-running session.
|
|
1733
|
+
sessionSafety: 'restart-required',
|
|
1734
|
+
restartRequired: true,
|
|
1735
|
+
sessionSafetyReason: 'Codex host cache generations are owned by Codex and have no lease API',
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
export function codexSessionSafety(status) {
|
|
1740
|
+
if (status?.restartRequired === true) return status;
|
|
1741
|
+
return {
|
|
1742
|
+
sessionSafety: 'unknown',
|
|
1743
|
+
restartRequired: false,
|
|
1744
|
+
sessionSafetyReason: 'No Codex plugin update occurred',
|
|
1745
|
+
};
|
|
1727
1746
|
}
|
|
1728
1747
|
|
|
1729
1748
|
function codexHooksList({
|
|
@@ -2857,6 +2876,31 @@ function missingUpdaterHelp(kbDir) {
|
|
|
2857
2876
|
console.error(` — the current bundle ships forge-update.mjs; then this command will work.`);
|
|
2858
2877
|
}
|
|
2859
2878
|
|
|
2879
|
+
function acquireHostConvergenceLock(lockPath) {
|
|
2880
|
+
try {
|
|
2881
|
+
fs.mkdirSync(lockPath);
|
|
2882
|
+
fs.writeFileSync(path.join(lockPath, 'owner.json'), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
2883
|
+
return { acquired: true, release: () => fs.rmSync(lockPath, { recursive: true, force: true }) };
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
if (error?.code !== 'EEXIST') return { acquired: false, error: error.message };
|
|
2886
|
+
try {
|
|
2887
|
+
const owner = JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
|
|
2888
|
+
if (Number.isInteger(owner.pid) && owner.pid !== process.pid) {
|
|
2889
|
+
try { process.kill(owner.pid, 0); return { acquired: false, error: `another host synchronization is running (pid ${owner.pid})` }; }
|
|
2890
|
+
catch (probeError) { if (probeError?.code !== 'ESRCH') return { acquired: false, error: 'another host synchronization owns the lock' }; }
|
|
2891
|
+
}
|
|
2892
|
+
} catch { /* an incomplete lock is safe to reclaim */ }
|
|
2893
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
2894
|
+
try {
|
|
2895
|
+
fs.mkdirSync(lockPath);
|
|
2896
|
+
fs.writeFileSync(path.join(lockPath, 'owner.json'), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
2897
|
+
return { acquired: true, release: () => fs.rmSync(lockPath, { recursive: true, force: true }) };
|
|
2898
|
+
} catch (retryError) {
|
|
2899
|
+
return { acquired: false, error: retryError.message };
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2860
2904
|
export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
2861
2905
|
sourceRoot = REPO_ROOT,
|
|
2862
2906
|
brainHome = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain'),
|
|
@@ -2864,6 +2908,7 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
2864
2908
|
wireClaude = wirePlugin,
|
|
2865
2909
|
wireCodexHost: detectCodexHost = wireCodexHost,
|
|
2866
2910
|
wireCodexPlugin: installCodexPlugin = wireCodexPlugin,
|
|
2911
|
+
hostLockPath = path.join(brainHome, 'host-convergence.lock'),
|
|
2867
2912
|
runStableSpine = (apply) => spawnSync(
|
|
2868
2913
|
process.execPath,
|
|
2869
2914
|
[apply, '--auto', '--expected-version', PACKAGE_VERSION],
|
|
@@ -2871,14 +2916,18 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
2871
2916
|
),
|
|
2872
2917
|
} = {}) {
|
|
2873
2918
|
const results = {};
|
|
2919
|
+
const hostLock = acquireHostConvergenceLock(hostLockPath);
|
|
2920
|
+
if (!hostLock.acquired) return { ok: false, results, error: `host synchronization lock unavailable: ${hostLock.error}` };
|
|
2874
2921
|
let runtimeTransaction;
|
|
2875
2922
|
try {
|
|
2876
2923
|
runtimeTransaction = beginConsoleRuntimeTransaction(cacheDir, sourceRoot);
|
|
2877
2924
|
} catch (error) {
|
|
2925
|
+
hostLock.release();
|
|
2878
2926
|
return { ok: false, results, error: `Console runtime staging failed: ${error.message}` };
|
|
2879
2927
|
}
|
|
2880
2928
|
const fail = (detail = {}) => {
|
|
2881
2929
|
runtimeTransaction.rollback();
|
|
2930
|
+
hostLock.release();
|
|
2882
2931
|
return { ok: false, results, ...detail };
|
|
2883
2932
|
};
|
|
2884
2933
|
|
|
@@ -2959,6 +3008,7 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
2959
3008
|
},
|
|
2960
3009
|
consoleRuntime: results.consoleRuntime,
|
|
2961
3010
|
});
|
|
3011
|
+
hostLock.release();
|
|
2962
3012
|
return { ok: convergence.healthy === true, convergence, results, applyStatus: applied.status,
|
|
2963
3013
|
error: convergence.healthy === true ? null : `host convergence is ${convergence.state}: ${convergence.action}` };
|
|
2964
3014
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.16",
|
|
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": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"test:regression": "vitest run tests/regression",
|
|
33
33
|
"qe:ux": "node scripts/qe/ux-suite.mjs",
|
|
34
34
|
"test:cov": "vitest run tests/unit --coverage",
|
|
35
|
+
"coverage:source": "node scripts/source-coverage.mjs --check",
|
|
35
36
|
"test:all": "npm run test:unit && npm run test:mesh && npm run test:mutation && npm run test:regression && npm run test:integration && npm test",
|
|
36
37
|
"metaharness:receipts": "node scripts/metaharness-receipts.mjs",
|
|
37
38
|
"route:cheap": "node scripts/route-cheap.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.16",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
package/plugin/mcp/server.mjs
CHANGED
|
@@ -343,7 +343,7 @@ async function handleClient(msg) {
|
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
345
|
if (!c) {
|
|
346
|
-
return clientOk(id, { content: [{ type: 'text', text: `search_ruvnet error: the brain bundle is
|
|
346
|
+
return clientOk(id, { content: [{ type: 'text', text: `search_ruvnet error: the brain bundle is unavailable at ${KB}. Diagnose the active installation and KB path before choosing a repair.` }], isError: true });
|
|
347
347
|
}
|
|
348
348
|
pendingCount++;
|
|
349
349
|
try {
|
|
@@ -106,6 +106,11 @@ verification → at most one revision. Both hosts design the ADR, DDD and intend
|
|
|
106
106
|
Agentic-QE then generates and executes the measurable test artifacts. A host merely completing is
|
|
107
107
|
not an accepted outcome.
|
|
108
108
|
|
|
109
|
+
When the duel returns `learningPersistenceRequest`, pass that request to Ruflo's structured
|
|
110
|
+
`memory_store` tool and verify the exact returned key. The coordinator never starts a second Ruflo
|
|
111
|
+
memory driver itself; without an MCP-aware caller it leaves `learningPersisted: false` and prints
|
|
112
|
+
the pending request so the completed duel is not mistaken for a persisted learning event.
|
|
113
|
+
|
|
109
114
|
Subscription-only is a billing boundary: verify with `claude auth status --json` and
|
|
110
115
|
`codex login status`, strip provider API-key variables from both child processes, and never fall back
|
|
111
116
|
to an API when a subscription is absent or capacity-limited. Subscription use consumes plan
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Dream Cycle issue disposition gate.
|
|
2
|
+
//
|
|
3
|
+
// The nightly engine may discover hypotheses, failed probes, or already-fixed findings. None of
|
|
4
|
+
// those is sufficient justification for creating a public issue. This module is deliberately pure:
|
|
5
|
+
// the caller supplies the finding and the current open issues, and receives a disposition before
|
|
6
|
+
// invoking any GitHub write operation.
|
|
7
|
+
|
|
8
|
+
const HEX_SHA = /^[0-9a-f]{7,64}$/i;
|
|
9
|
+
const ISSUE_MARKER = 'dream-fingerprint:';
|
|
10
|
+
|
|
11
|
+
function text(value) {
|
|
12
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Stable identity used by the ledger, issue body, and open-issue deduplication. */
|
|
16
|
+
export function stableFingerprint(finding = {}) {
|
|
17
|
+
const parts = ['deep', 'scan', 'path', 'signature'].map((key) => text(finding[key]));
|
|
18
|
+
return parts.every(Boolean) ? parts.join('|') : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function issueBodyMarker(fingerprint) {
|
|
22
|
+
return fingerprint ? `${ISSUE_MARKER} ${fingerprint}` : '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sourceBacked(finding) {
|
|
26
|
+
const sourceSha = text(finding.sourceSha || finding.sourceCommit || finding.source?.commit);
|
|
27
|
+
const sourcePaths = finding.sourcePaths || finding.paths || finding.source?.paths;
|
|
28
|
+
const reproduction = finding.reproduction || finding.repro;
|
|
29
|
+
const evidence = Array.isArray(finding.evidence) ? finding.evidence : [];
|
|
30
|
+
if (!HEX_SHA.test(sourceSha)) return false;
|
|
31
|
+
if (!Array.isArray(sourcePaths) || sourcePaths.length === 0 || sourcePaths.some((p) => !text(p))) return false;
|
|
32
|
+
if (!reproduction || !text(reproduction.command) || !text(reproduction.output)) return false;
|
|
33
|
+
return evidence.length > 0 && evidence.every((item) => item && text(item.observed)
|
|
34
|
+
&& (text(item.path) || text(item.source) || text(item.sourcePath)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function openIssueFor(fingerprint, openIssues) {
|
|
38
|
+
if (!fingerprint || !Array.isArray(openIssues)) return null;
|
|
39
|
+
const marker = issueBodyMarker(fingerprint);
|
|
40
|
+
return openIssues.find((issue) => text(issue.body).includes(marker)
|
|
41
|
+
|| text(issue.title).includes(marker)
|
|
42
|
+
|| text(issue.fingerprint) === fingerprint) || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decide whether a finding may become a GitHub issue.
|
|
47
|
+
*
|
|
48
|
+
* `report` is the safe destination for hypotheses, missing evidence, resolved findings, and
|
|
49
|
+
* findings that are not yet actionable. `dedupe` points at the existing open issue. Only `create`
|
|
50
|
+
* is permission for the caller to invoke `gh issue create`.
|
|
51
|
+
*/
|
|
52
|
+
export function assessFinding(finding = {}, { openIssues = [] } = {}) {
|
|
53
|
+
const fingerprint = stableFingerprint(finding);
|
|
54
|
+
const common = { fingerprint: fingerprint || null };
|
|
55
|
+
const status = text(finding.status || finding.state).toLowerCase();
|
|
56
|
+
const skip = new Set((finding.skipIf || []).map((value) => text(value).toLowerCase()));
|
|
57
|
+
|
|
58
|
+
if (!fingerprint) return { ...common, action: 'report', reason: 'missing-stable-fingerprint' };
|
|
59
|
+
if (status === 'hypothesis' || status === 'unreproduced' || status === 'unknown') {
|
|
60
|
+
return { ...common, action: 'report', reason: 'finding-not-reproduced' };
|
|
61
|
+
}
|
|
62
|
+
if (!sourceBacked(finding)) return { ...common, action: 'report', reason: 'missing-source-backed-reproduction' };
|
|
63
|
+
if (skip.has('already-fixed') || skip.has('verified-local-fix') || skip.has('environment-only')
|
|
64
|
+
|| finding.resolved === true || finding.fixed === true) {
|
|
65
|
+
return { ...common, action: 'report', reason: 'resolved-or-non-defect' };
|
|
66
|
+
}
|
|
67
|
+
if (status !== 'reproduced' && status !== 'reproducible' && status !== 'confirmed') {
|
|
68
|
+
return { ...common, action: 'report', reason: 'finding-not-confirmed' };
|
|
69
|
+
}
|
|
70
|
+
if (finding.actionable !== true || finding.unresolved !== true || finding.boundedFixUnsuccessful !== true) {
|
|
71
|
+
return { ...common, action: 'report', reason: 'repair-or-actionability-evidence-missing' };
|
|
72
|
+
}
|
|
73
|
+
const existing = openIssueFor(fingerprint, openIssues);
|
|
74
|
+
if (existing) return { ...common, action: 'dedupe', reason: 'duplicate-open-issue', issue: existing };
|
|
75
|
+
return { ...common, action: 'create', reason: 'reproduced-actionable-unresolved-after-bounded-repair' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildIssueBody(finding, decision = assessFinding(finding)) {
|
|
79
|
+
if (decision.action !== 'create') throw new Error(`cannot build issue body for ${decision.action}`);
|
|
80
|
+
const sourceSha = text(finding.sourceSha || finding.sourceCommit || finding.source?.commit);
|
|
81
|
+
return [
|
|
82
|
+
`<!-- ${issueBodyMarker(decision.fingerprint)} -->`,
|
|
83
|
+
`## ${text(finding.title) || 'Dream Cycle finding'}`,
|
|
84
|
+
'',
|
|
85
|
+
text(finding.summary || finding.description),
|
|
86
|
+
'',
|
|
87
|
+
`Source: ${sourceSha}`,
|
|
88
|
+
`Paths: ${(finding.sourcePaths || finding.paths || []).join(', ')}`,
|
|
89
|
+
`Reproduction: \`${text(finding.reproduction?.command)}\``,
|
|
90
|
+
'',
|
|
91
|
+
'Observed output:',
|
|
92
|
+
'```',
|
|
93
|
+
text(finding.reproduction?.output),
|
|
94
|
+
'```',
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Adapter contract for the Dream Machine runner. Input is JSON on stdin:
|
|
99
|
+
// { "finding": { ... }, "openIssues": [{ "number", "title", "body" }] }
|
|
100
|
+
// Output is the disposition JSON. This command intentionally cannot create or mutate GitHub data.
|
|
101
|
+
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
|
102
|
+
let input = '';
|
|
103
|
+
process.stdin.setEncoding('utf8');
|
|
104
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
105
|
+
process.stdin.on('end', () => {
|
|
106
|
+
try {
|
|
107
|
+
const payload = JSON.parse(input || '{}');
|
|
108
|
+
const decision = assessFinding(payload.finding || payload, { openIssues: payload.openIssues || [] });
|
|
109
|
+
process.stdout.write(`${JSON.stringify(decision)}\n`);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
process.stderr.write(`dream-issue-gate: invalid JSON input: ${error.message}\n`);
|
|
112
|
+
process.exitCode = 2;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { spawn
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { probeSubscriptionHosts, subscriptionOnlyEnv } from './subscription-hosts.mjs';
|
|
7
7
|
|
|
@@ -67,15 +67,34 @@ function parseHostValue(host, stdout) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function spawnHost(binary, args, options) {
|
|
70
|
+
function spawnHost(binary, args, options, input = '') {
|
|
71
71
|
return new Promise((resolve) => {
|
|
72
72
|
const child = spawn(binary, args, options);
|
|
73
73
|
let stdout = '';
|
|
74
74
|
let stderr = '';
|
|
75
|
+
let inputError = null;
|
|
76
|
+
let settled = false;
|
|
77
|
+
const finish = (result) => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
resolve(result);
|
|
81
|
+
};
|
|
75
82
|
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
76
83
|
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
77
|
-
child.on('error', (error) =>
|
|
78
|
-
|
|
84
|
+
child.stdin.on('error', (error) => {
|
|
85
|
+
inputError = error;
|
|
86
|
+
stderr += `${stderr ? '\n' : ''}${error.message}`;
|
|
87
|
+
try { child.kill(); } catch { /* child may already be gone */ }
|
|
88
|
+
});
|
|
89
|
+
child.on('error', (error) => finish({ status: null, stdout, stderr: error.message, error }));
|
|
90
|
+
try {
|
|
91
|
+
child.stdin.end(input);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
inputError = error;
|
|
94
|
+
stderr += `${stderr ? '\n' : ''}${error.message}`;
|
|
95
|
+
try { child.kill(); } catch { /* child may already be gone */ }
|
|
96
|
+
}
|
|
97
|
+
child.on('close', (status) => finish({ status, stdout, stderr, error: inputError }));
|
|
79
98
|
});
|
|
80
99
|
}
|
|
81
100
|
|
|
@@ -87,20 +106,21 @@ export async function runSubscriptionHost(host, stage, payload, { cwd = process.
|
|
|
87
106
|
binary: 'claude',
|
|
88
107
|
args: [
|
|
89
108
|
'-p', '--output-format', 'json', '--permission-mode', 'plan',
|
|
90
|
-
'--tools', 'Read,Grep,Glob', '--no-session-persistence', '--effort', 'high',
|
|
109
|
+
'--tools', 'Read,Grep,Glob', '--no-session-persistence', '--effort', 'high',
|
|
91
110
|
],
|
|
92
111
|
}
|
|
93
112
|
: {
|
|
94
113
|
binary: 'codex',
|
|
95
114
|
args: [
|
|
96
115
|
'exec', '--ephemeral', '--sandbox', 'read-only', '--color', 'never', '--json',
|
|
97
|
-
'-m', 'gpt-5.6-sol', '-c', 'model_reasoning_effort="high"',
|
|
116
|
+
'-m', 'gpt-5.6-sol', '-c', 'model_reasoning_effort="high"',
|
|
98
117
|
],
|
|
99
118
|
};
|
|
100
|
-
const result = await spawnHost(command.binary, command.args, { cwd, env, stdio: ['
|
|
119
|
+
const result = await spawnHost(command.binary, command.args, { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] }, prompt);
|
|
101
120
|
if (result.status !== 0) {
|
|
102
121
|
return {
|
|
103
122
|
ok: false,
|
|
123
|
+
error: result.stderr || `host exited without a status (${result.status})`,
|
|
104
124
|
reason: /limit|quota|capacity|usage/i.test(result.stderr)
|
|
105
125
|
? 'capacity-limited'
|
|
106
126
|
: 'host-failed',
|
|
@@ -109,11 +129,7 @@ export async function runSubscriptionHost(host, stage, payload, { cwd = process.
|
|
|
109
129
|
return { ok: true, value: parseHostValue(host, result.stdout) };
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
export
|
|
113
|
-
cwd = process.cwd(),
|
|
114
|
-
now = Date.now,
|
|
115
|
-
run = spawnSync,
|
|
116
|
-
} = {}) {
|
|
132
|
+
export function deliberationMemoryStoreRequest(receipt, { now = Date.now } = {}) {
|
|
117
133
|
const recordedAt = now();
|
|
118
134
|
const value = {
|
|
119
135
|
protocol: receipt.protocol,
|
|
@@ -125,21 +141,28 @@ export async function persistDeliberationReceipt(receipt, {
|
|
|
125
141
|
recordedAt: new Date(recordedAt).toISOString(),
|
|
126
142
|
};
|
|
127
143
|
const key = `dual-deliberation-${recordedAt}-${receipt.taskHash.slice(0, 12)}`;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
'
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
return {
|
|
145
|
+
tool: 'memory_store',
|
|
146
|
+
name: 'memory_store',
|
|
147
|
+
arguments: { key, value: JSON.stringify(value), namespace: 'ruvnet-brain' },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function persistenceProof(proof, key) {
|
|
152
|
+
return proof === true || (proof?.stored === true && proof?.verified === true && proof?.key === key);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function persistDeliberationReceipt(receipt, {
|
|
156
|
+
now = Date.now,
|
|
157
|
+
memoryStore,
|
|
158
|
+
} = {}) {
|
|
159
|
+
const request = deliberationMemoryStoreRequest(receipt, { now });
|
|
160
|
+
if (typeof memoryStore !== 'function') return request;
|
|
161
|
+
try {
|
|
162
|
+
return persistenceProof(await memoryStore(request), request.arguments.key);
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
143
166
|
}
|
|
144
167
|
|
|
145
168
|
function missingHosts(probes) {
|
|
@@ -164,7 +187,6 @@ export async function deliberate(task, options = {}) {
|
|
|
164
187
|
runSubscriptionHost(host, stage, payload, { cwd: options.cwd })
|
|
165
188
|
));
|
|
166
189
|
const cwd = options.cwd ?? process.cwd();
|
|
167
|
-
const persist = options.persist ?? ((receipt) => persistDeliberationReceipt(receipt, { cwd }));
|
|
168
190
|
const eligibleHosts = HOSTS.filter((host) => probes[hostKey(host)]?.eligible);
|
|
169
191
|
|
|
170
192
|
if (eligibleHosts.length === 0) {
|
|
@@ -247,7 +269,18 @@ export async function deliberate(task, options = {}) {
|
|
|
247
269
|
roles,
|
|
248
270
|
accepted,
|
|
249
271
|
};
|
|
250
|
-
const
|
|
272
|
+
const learningPersistenceRequest = accepted
|
|
273
|
+
? deliberationMemoryStoreRequest(receipt, { now: options.now ?? Date.now })
|
|
274
|
+
: undefined;
|
|
275
|
+
let learningPersisted = false;
|
|
276
|
+
if (accepted && typeof options.persist === 'function') {
|
|
277
|
+
try {
|
|
278
|
+
const persisted = await options.persist(learningPersistenceRequest, receipt);
|
|
279
|
+
learningPersisted = persistenceProof(persisted, learningPersistenceRequest.arguments.key);
|
|
280
|
+
} catch {
|
|
281
|
+
learningPersisted = false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
251
284
|
return {
|
|
252
285
|
status: accepted ? 'accepted' : 'unresolved',
|
|
253
286
|
dual: true,
|
|
@@ -256,6 +289,7 @@ export async function deliberate(task, options = {}) {
|
|
|
256
289
|
verification: verification.ok ? verification.value : undefined,
|
|
257
290
|
verifiedOutcome: accepted,
|
|
258
291
|
learningPersisted,
|
|
292
|
+
...(learningPersistenceRequest ? { learningPersistenceRequest } : {}),
|
|
259
293
|
};
|
|
260
294
|
}
|
|
261
295
|
|
|
@@ -37,9 +37,15 @@ export function observeNpxPackage(npmCache, expected) {
|
|
|
37
37
|
export function validateNpxObservations(proof) {
|
|
38
38
|
const expected = proof.packageExecution?.expected;
|
|
39
39
|
const observations = proof.packageExecution?.observations;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const policy = proof.packageExecution?.policy;
|
|
41
|
+
const target = proof.registration?.packageTarget;
|
|
42
|
+
const productionLatest = policy === 'production-latest-exact-cache-v1'
|
|
43
|
+
&& target?.spec === 'ruvnet-brain@latest' && target.sha256 === null;
|
|
44
|
+
const sealedCandidate = policy === 'sealed-candidate-exact-cache-v1'
|
|
45
|
+
&& typeof target?.spec === 'string' && target.spec.endsWith('.tgz')
|
|
46
|
+
&& /^[a-f0-9]{64}$/.test(String(target.sha256 || ''))
|
|
47
|
+
&& target.sha256 === proof.candidate?.sha256;
|
|
48
|
+
if ((!productionLatest && !sealedCandidate)
|
|
43
49
|
|| expected?.packageSha256 !== proof.candidate?.sha256
|
|
44
50
|
|| expected?.name !== 'ruvnet-brain' || expected.version !== proof.candidate?.version
|
|
45
51
|
|| !/^[a-f0-9]{64}$/.test(expected.treeSha256 || '') || !Number.isSafeInteger(expected.fileCount)
|
|
@@ -180,6 +180,127 @@ export async function triggerNativeRun({ scheduler, registration, platform, env,
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Fast release proof for the native scheduler boundary. This deliberately checks only the
|
|
185
|
+
* scheduler's real registration, load, trigger dispatch, and cleanup. The expensive two-run
|
|
186
|
+
* updater lifecycle remains a scheduled/post-publication soak and must not block every release.
|
|
187
|
+
*/
|
|
188
|
+
export async function runNativeSchedulerSmoke({ packageRoot, packagePath, bundlePath, sourceSha, workflowRunId,
|
|
189
|
+
scheduler, env, brainHome, kbDir, now = Date.now, command = run } = {}) {
|
|
190
|
+
if (!['darwin', 'linux', 'win32'].includes(process.platform)) {
|
|
191
|
+
throw new Error(`unsupported native scheduler platform: ${process.platform}`);
|
|
192
|
+
}
|
|
193
|
+
if (!packageRoot) throw new Error('scheduler smoke package root is missing');
|
|
194
|
+
const packageRootStat = fs.lstatSync(packageRoot);
|
|
195
|
+
if (!packageRootStat.isDirectory() || packageRootStat.isSymbolicLink()) {
|
|
196
|
+
throw new Error('scheduler smoke package root is not a regular directory');
|
|
197
|
+
}
|
|
198
|
+
const packageArchive = path.resolve(packagePath || '');
|
|
199
|
+
const packageStat = fs.lstatSync(packageArchive);
|
|
200
|
+
if (!packageStat.isFile() || packageStat.isSymbolicLink() || !packageArchive.endsWith('.tgz')) {
|
|
201
|
+
throw new Error('scheduler smoke package archive is not a regular file');
|
|
202
|
+
}
|
|
203
|
+
const bundle = path.resolve(bundlePath || '');
|
|
204
|
+
const stat = fs.lstatSync(bundle);
|
|
205
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('scheduler smoke bundle is not a regular file');
|
|
206
|
+
if (!/^[a-f0-9]{40}$/.test(String(sourceSha || ''))) throw new Error('scheduler smoke source SHA is invalid');
|
|
207
|
+
if (!/^\d+$/.test(String(workflowRunId || ''))) throw new Error('scheduler smoke workflow run ID is invalid');
|
|
208
|
+
if (!scheduler || typeof scheduler.installNightlyRunner !== 'function'
|
|
209
|
+
|| typeof scheduler.installScheduler !== 'function' || typeof scheduler.schedulerStatus !== 'function'
|
|
210
|
+
|| typeof scheduler.removeScheduler !== 'function') throw new Error('scheduler smoke adapter is incomplete');
|
|
211
|
+
|
|
212
|
+
const identity = `com.ruvnet.brain-update.proof-smoke-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
213
|
+
const packageSha256 = sha256(fs.readFileSync(packageArchive));
|
|
214
|
+
const bundleSha256 = sha256(fs.readFileSync(bundle));
|
|
215
|
+
const registration = scheduler.installNightlyRunner({ brainHome,
|
|
216
|
+
source: path.join(packageRoot, 'bin', 'nightly-refresh.mjs'), nodePath: process.execPath, identity, env,
|
|
217
|
+
packageTarget: { spec: packageArchive, sha256: packageSha256 },
|
|
218
|
+
bundleTarget: { spec: bundle, sha256: bundleSha256 } });
|
|
219
|
+
// A hosted runner can expose a native scheduler binary whose IPC endpoint is unavailable.
|
|
220
|
+
// Every scheduler operation is therefore bounded independently; a stuck launchd/schtasks
|
|
221
|
+
// call must become a typed failure, never consume the entire release job timeout.
|
|
222
|
+
const schedulerRun = (name, args, options = {}) => spawnSync(name, args, {
|
|
223
|
+
encoding: 'utf8', timeout: 30_000, ...options,
|
|
224
|
+
});
|
|
225
|
+
const options = { platform: process.platform, env, brainHome, kbDir, identity, run: schedulerRun,
|
|
226
|
+
proofTick: process.platform === 'linux', proofAt: process.platform === 'linux' ? now() + 60_000 : undefined,
|
|
227
|
+
pathValue: env.PATH };
|
|
228
|
+
const before = scheduler.schedulerStatus(options);
|
|
229
|
+
if (before.state !== 'off') throw new Error(`scheduler smoke identity is not absent: ${before.evidence}`);
|
|
230
|
+
let installed = false;
|
|
231
|
+
let cleaned = false;
|
|
232
|
+
try {
|
|
233
|
+
const result = scheduler.installScheduler(registration, options);
|
|
234
|
+
if (!result.ok) throw new Error(`native scheduler smoke install failed: ${result.why}`);
|
|
235
|
+
installed = true;
|
|
236
|
+
const loaded = scheduler.schedulerStatus(options);
|
|
237
|
+
if (loaded.state !== 'on') throw new Error(`native scheduler smoke was not loaded: ${loaded.evidence}`);
|
|
238
|
+
if (process.platform === 'darwin') {
|
|
239
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
240
|
+
command('launchctl', ['kickstart', '-k', `gui/${uid}/${identity}`], { timeout: 30_000 });
|
|
241
|
+
} else if (process.platform === 'win32') {
|
|
242
|
+
command('schtasks', ['/Run', '/TN', identity], { timeout: 30_000 });
|
|
243
|
+
}
|
|
244
|
+
const removed = scheduler.removeScheduler({ ...options, platform: process.platform, env, identity });
|
|
245
|
+
if (!removed.ok) throw new Error(`native scheduler smoke cleanup failed: ${removed.why}`);
|
|
246
|
+
cleaned = true;
|
|
247
|
+
const absent = scheduler.schedulerStatus(options);
|
|
248
|
+
if (absent.state !== 'off') throw new Error(`native scheduler smoke absence was not verified: ${absent.evidence}`);
|
|
249
|
+
return {
|
|
250
|
+
schemaVersion: 1,
|
|
251
|
+
kind: 'ruvnet-brain-native-scheduler-smoke',
|
|
252
|
+
scope: 'public-release-scheduler-boundary',
|
|
253
|
+
platform: process.platform,
|
|
254
|
+
sourceSha: String(sourceSha),
|
|
255
|
+
workflowRunId: String(workflowRunId),
|
|
256
|
+
identity,
|
|
257
|
+
registration: {
|
|
258
|
+
recordPath: registration.recordPath, nodePath: registration.nodePath,
|
|
259
|
+
runnerPath: registration.runnerPath, runnerSha256: registration.runnerSha256,
|
|
260
|
+
packageTarget: registration.packageTarget, bundleTarget: registration.bundleTarget,
|
|
261
|
+
},
|
|
262
|
+
trigger: { kind: { darwin: 'launchctl-kickstart', linux: 'cron-registration', win32: 'schtasks-run' }[process.platform], identity },
|
|
263
|
+
loaded: true,
|
|
264
|
+
cleaned: true,
|
|
265
|
+
observedAt: new Date().toISOString(),
|
|
266
|
+
};
|
|
267
|
+
} finally {
|
|
268
|
+
if (installed && !cleaned) {
|
|
269
|
+
try { scheduler.removeScheduler({ ...options, platform: process.platform, env, identity }); } catch { /* preserve original failure */ }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function validateNativeSchedulerSmoke(smoke, { platform, sourceSha, workflowRunId,
|
|
275
|
+
bundleSha256, packageSha256 } = {}) {
|
|
276
|
+
if (!smoke || typeof smoke !== 'object') {
|
|
277
|
+
return { ok: false, failures: ['invalid native nightly proof: scheduler smoke receipt is missing'] };
|
|
278
|
+
}
|
|
279
|
+
const failures = [];
|
|
280
|
+
if (smoke?.schemaVersion !== 1 || smoke?.kind !== 'ruvnet-brain-native-scheduler-smoke'
|
|
281
|
+
|| smoke.scope !== 'public-release-scheduler-boundary') failures.push('scheduler smoke envelope is invalid');
|
|
282
|
+
if (smoke.platform !== platform || smoke.sourceSha !== sourceSha
|
|
283
|
+
|| String(smoke.workflowRunId || '') !== String(workflowRunId || '')) failures.push('scheduler smoke identity differs');
|
|
284
|
+
if (!/^com\.ruvnet\.brain-update\.proof-smoke-[A-Za-z0-9._-]+$/.test(smoke.identity || '')) failures.push('scheduler smoke identity is not isolated');
|
|
285
|
+
if (smoke.loaded !== true || smoke.cleaned !== true || smoke.trigger?.identity !== smoke.identity) failures.push('scheduler smoke did not prove load, trigger, and cleanup');
|
|
286
|
+
const expectedTrigger = { darwin: 'launchctl-kickstart', linux: 'cron-registration', win32: 'schtasks-run' }[platform];
|
|
287
|
+
if (smoke.trigger?.kind !== expectedTrigger) failures.push('scheduler smoke trigger differs');
|
|
288
|
+
const registration = smoke.registration;
|
|
289
|
+
// Receipts from a Windows runner are finalized on Ubuntu. Validate the path syntax emitted by
|
|
290
|
+
// the producing OS instead of applying the verifier host's POSIX rules to drive-letter paths.
|
|
291
|
+
const nativePath = platform === 'win32' ? path.win32 : path;
|
|
292
|
+
if (!registration || !/^[a-f0-9]{64}$/.test(registration.runnerSha256 || '')
|
|
293
|
+
|| !/^[a-f0-9]{64}$/.test(registration.bundleTarget?.sha256 || '')
|
|
294
|
+
|| registration.bundleTarget.sha256 !== bundleSha256
|
|
295
|
+
|| !/^[a-f0-9]{64}$/.test(registration.packageTarget?.sha256 || '')
|
|
296
|
+
|| (packageSha256 !== undefined && registration.packageTarget.sha256 !== packageSha256)
|
|
297
|
+
|| !nativePath.isAbsolute(registration.packageTarget?.spec || '') || !registration.packageTarget.spec.endsWith('.tgz')
|
|
298
|
+
|| !nativePath.isAbsolute(registration.recordPath || '') || !nativePath.isAbsolute(registration.nodePath || '')
|
|
299
|
+
|| !nativePath.isAbsolute(registration.runnerPath || '')) failures.push('scheduler smoke registration identity is incomplete');
|
|
300
|
+
if (!Number.isFinite(Date.parse(smoke.observedAt)) || Date.parse(smoke.observedAt) > Date.now() + 60_000) failures.push('scheduler smoke observation time is invalid');
|
|
301
|
+
return { ok: failures.length === 0, failures };
|
|
302
|
+
}
|
|
303
|
+
|
|
183
304
|
export function validateTwoRunEvidence({ first, second, inventoryBefore, inventoryAfterFirst,
|
|
184
305
|
inventoryAfterSecond, retention, validateEnvelope, identity }) {
|
|
185
306
|
const failures = [];
|
|
@@ -385,8 +506,15 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
|
|
|
385
506
|
const packageRoot = path.join(prefix, 'node_modules', 'ruvnet-brain');
|
|
386
507
|
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
387
508
|
const expectedPackage = packageTreeObservation(packageRoot);
|
|
388
|
-
//
|
|
389
|
-
|
|
509
|
+
// Release verification already downloaded and hashed the exact npm bytes above. Reusing
|
|
510
|
+
// those bytes keeps the per-release scheduler smoke deterministic and avoids a second
|
|
511
|
+
// registry/latest resolution. The unattended production nightly still uses @latest.
|
|
512
|
+
const packageTarget = {
|
|
513
|
+
spec: candidate,
|
|
514
|
+
sha256: packageSha256,
|
|
515
|
+
policy: 'sealed-candidate-exact-cache-v1',
|
|
516
|
+
};
|
|
517
|
+
const prepare = npmInvocation(['exec', '--yes', `--package=${packageTarget.spec}`, '--', 'ruvnet-brain', '--help']);
|
|
390
518
|
run(prepare.executable, prepare.args, { env, cwd: home });
|
|
391
519
|
const packageObservations = [observeNpxPackage(npmCache, expectedPackage)];
|
|
392
520
|
// Register the exact public package's plugin in this fresh home. GitHub shorthand can
|
|
@@ -400,7 +528,7 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
|
|
|
400
528
|
scheduler = await import(`${pathToFileURL(path.join(packageRoot, 'plugin', 'scripts', 'nightly-scheduler.mjs')).href}?proof=${Date.now()}`);
|
|
401
529
|
registration = scheduler.installNightlyRunner({ brainHome,
|
|
402
530
|
source: path.join(packageRoot, 'bin', 'nightly-refresh.mjs'), nodePath: process.execPath, identity, env,
|
|
403
|
-
packageTarget: { spec:
|
|
531
|
+
packageTarget: { spec: packageTarget.spec, sha256: packageTarget.sha256 },
|
|
404
532
|
bundleTarget: { spec: stagedBundle.sourcePath, sha256: stagedBundle.sha256 } });
|
|
405
533
|
const inventoryBefore = managedStorageInventory(kbDir);
|
|
406
534
|
const kick = () => triggerNativeRun({ scheduler, registration, platform: process.platform,
|
|
@@ -431,7 +559,7 @@ export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, time
|
|
|
431
559
|
kind: 'ruvnet-brain-native-two-run-nightly-proof',
|
|
432
560
|
scope: 'installed-update',
|
|
433
561
|
installationConfiguration: { npmBinLinks: false, source: 'initial-reader-install-only', removedEmptyScaffolds },
|
|
434
|
-
packageExecution: { policy:
|
|
562
|
+
packageExecution: { policy: packageTarget.policy,
|
|
435
563
|
expected: { ...expectedPackage, packageSha256 }, observations: packageObservations },
|
|
436
564
|
upstreamFreshness: 'UNKNOWN',
|
|
437
565
|
observedAt: new Date().toISOString(),
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { canonicalJson, digest } from './coverage-integrity.mjs';
|
|
7
7
|
import { retrievalOracleExpectationFromPlan, validateRetrievalOracleReview } from './independent-review-receipt.mjs';
|
|
8
8
|
import { validateRetrievalCanaryPlan, validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
|
|
9
|
-
import { validateNightlyProofReceipt } from './nightly-two-run-proof.mjs';
|
|
9
|
+
import { validateNightlyProofReceipt, validateNativeSchedulerSmoke } from './nightly-two-run-proof.mjs';
|
|
10
10
|
|
|
11
11
|
export const PUBLIC_VERIFICATION_OS = Object.freeze(['linux', 'macos', 'windows']);
|
|
12
12
|
export const PUBLIC_VERIFICATION_MODES = Object.freeze(['claude', 'codex', 'dual']);
|
|
@@ -63,12 +63,21 @@ export function validatePublicVerificationLeaf(leaf, { publicKey } = {}) {
|
|
|
63
63
|
}
|
|
64
64
|
if (leaf.mode === 'dual') {
|
|
65
65
|
if (!/^[1-9][0-9]*$/.test(String(leaf.workflowRunId || ''))) throw new Error('native nightly workflow run identity is missing');
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
if (leaf.nativeNightly !== undefined) {
|
|
67
|
+
const native = validateNightlyProofReceipt(leaf.nativeNightly, {
|
|
68
|
+
platform: { linux: 'linux', macos: 'darwin', windows: 'win32' }[leaf.os],
|
|
69
|
+
version: leaf.version, sourceSha: leaf.sourceSha, workflowRunId: leaf.workflowRunId,
|
|
70
|
+
packageSha256: leaf.artifactSha256, bundleSha256: leaf.bundleSha256, publicKey,
|
|
71
|
+
});
|
|
72
|
+
if (!native.ok) throw new Error(`${leaf.os}/dual native nightly proof failed: ${native.failures.join('; ')}`);
|
|
73
|
+
} else {
|
|
74
|
+
const smoke = validateNativeSchedulerSmoke(leaf.nativeSchedulerSmoke, {
|
|
75
|
+
platform: { linux: 'linux', macos: 'darwin', windows: 'win32' }[leaf.os],
|
|
76
|
+
sourceSha: leaf.sourceSha, workflowRunId: leaf.workflowRunId, bundleSha256: leaf.bundleSha256,
|
|
77
|
+
packageSha256: leaf.artifactSha256,
|
|
78
|
+
});
|
|
79
|
+
if (!smoke.ok) throw new Error(`${leaf.os}/dual native scheduler smoke failed: ${smoke.failures.join('; ')}`);
|
|
80
|
+
}
|
|
72
81
|
} else if (leaf.nativeNightly !== undefined) {
|
|
73
82
|
throw new Error('native nightly proof belongs only to the dual-host leaf');
|
|
74
83
|
}
|
|
@@ -178,18 +178,26 @@ export async function createPublicVerificationLane({
|
|
|
178
178
|
failures.push({ mode, reason: error.message, retrieval });
|
|
179
179
|
continue;
|
|
180
180
|
}
|
|
181
|
-
// Native scheduler lifecycle is
|
|
182
|
-
//
|
|
183
|
-
//
|
|
181
|
+
// Native scheduler lifecycle is part of the accepted release contract. Keep
|
|
182
|
+
// the public lane bound to the same proof required by the leaf/aggregate
|
|
183
|
+
// validators; an adapter cannot silently downgrade a dual-host leaf.
|
|
184
184
|
let nativeNightly;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
let nativeSchedulerSmoke;
|
|
186
|
+
if (mode === 'dual') {
|
|
187
|
+
const schedulerMode = process.env.RUVNET_PUBLIC_SCHEDULER_MODE || 'full';
|
|
188
|
+
if (schedulerMode === 'smoke') {
|
|
189
|
+
if (typeof adapter.runNativeNightlySmoke !== 'function') throw new Error('native scheduler smoke adapter is required');
|
|
190
|
+
nativeSchedulerSmoke = await adapter.runNativeNightlySmoke({ identity, workflowRunId: String(workflowRunId) });
|
|
191
|
+
} else {
|
|
192
|
+
if (typeof adapter.runNativeNightly !== 'function') throw new Error('native nightly proof adapter is required');
|
|
193
|
+
nativeNightly = await adapter.runNativeNightly({ identity, workflowRunId: String(workflowRunId) });
|
|
194
|
+
}
|
|
188
195
|
}
|
|
189
196
|
leaves.push(createPublicVerificationLeaf({
|
|
190
197
|
...common,
|
|
191
198
|
...(publication.acceptancePolicy ? { acceptancePolicy: publication.acceptancePolicy, searchTiming: { firstSearchMs: installed.searchMs, broadMs: publication.brain.broadMs, deadlineMs: publication.brain.deadlineMs } } : {}),
|
|
192
199
|
...(nativeNightly === undefined ? {} : { nativeNightly }),
|
|
200
|
+
...(nativeSchedulerSmoke === undefined ? {} : { nativeSchedulerSmoke }),
|
|
193
201
|
os,
|
|
194
202
|
mode,
|
|
195
203
|
status: verifiedRetrieval.receiptSha256 === retrieval.receiptSha256 ? 'completed' : 'failed',
|
|
@@ -20,7 +20,12 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
20
20
|
import { evaluateCandidateReceipt, evaluatePublicationReceipt } from './release-proof.mjs';
|
|
21
21
|
import { verifyPayload } from './release-payload.mjs';
|
|
22
22
|
import { resolveInstalledCanaryCitation } from './retrieval-canary.mjs';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
runNativeSchedulerSmoke,
|
|
25
|
+
runNightlyTwoRunProof,
|
|
26
|
+
validateNativeSchedulerSmoke,
|
|
27
|
+
validateNightlyProofReceipt,
|
|
28
|
+
} from './nightly-two-run-proof.mjs';
|
|
24
29
|
import { parseRetrievalResult } from '../kb/retrieval-result.mjs';
|
|
25
30
|
|
|
26
31
|
const REPO = 'stuinfla/ruvnet-brain';
|
|
@@ -59,6 +64,39 @@ function command(name, args, options = {}) {
|
|
|
59
64
|
}
|
|
60
65
|
return String(result.stdout || '').trim();
|
|
61
66
|
}
|
|
67
|
+
|
|
68
|
+
// The public host matrix owns three independent install fixtures. Keep their long-running
|
|
69
|
+
// installer/doctor processes asynchronous: a synchronous child here would block Node's event loop,
|
|
70
|
+
// freezing an already-started MCP deadline while the next host is being installed. That was the
|
|
71
|
+
// concrete reason the macOS recovery lane reported a false 30-second Brain timeout after all three
|
|
72
|
+
// installs had succeeded. Short metadata probes still use command() above; only the host work uses
|
|
73
|
+
// this bounded async boundary.
|
|
74
|
+
function commandAsync(name, args, options = {}) {
|
|
75
|
+
const { platform = process.platform, timeout, ...spawnOptions } = options;
|
|
76
|
+
const invocation = commandInvocation(name, args, { platform, env: spawnOptions.env || process.env });
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const child = spawn(invocation.executable, invocation.args, spawnOptions);
|
|
79
|
+
let settled = false;
|
|
80
|
+
let timer;
|
|
81
|
+
const finish = (error, value = '') => {
|
|
82
|
+
if (settled) return;
|
|
83
|
+
settled = true;
|
|
84
|
+
if (timer) clearTimeout(timer);
|
|
85
|
+
error ? reject(error) : resolve(value);
|
|
86
|
+
};
|
|
87
|
+
if (timeout !== undefined) {
|
|
88
|
+
timer = setTimeout(() => {
|
|
89
|
+
child.kill('SIGTERM');
|
|
90
|
+
finish(new Error(`${name} ${args.join(' ')} exceeded ${timeout}ms deadline`));
|
|
91
|
+
}, timeout);
|
|
92
|
+
}
|
|
93
|
+
child.once('error', (error) => finish(error));
|
|
94
|
+
child.once('close', (code, signal) => {
|
|
95
|
+
if (code === 0) finish(null);
|
|
96
|
+
else finish(new Error(`${name} ${args.join(' ')} failed: ${signal || `exit ${code}`}`));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
62
100
|
export function validateCandidateSource(root, { sha, version }) {
|
|
63
101
|
const candidateRoot = fs.realpathSync(root);
|
|
64
102
|
const options = { cwd: candidateRoot };
|
|
@@ -302,8 +340,9 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
302
340
|
const mcpSessions = new Map();
|
|
303
341
|
const passageFileDigests = new Map();
|
|
304
342
|
let installTemp = null;
|
|
343
|
+
let installedPackageRoot = null;
|
|
344
|
+
let installedBundlePath = null;
|
|
305
345
|
return {
|
|
306
|
-
publicNativeNightly: false,
|
|
307
346
|
async downloadNpm({ version, destination }) {
|
|
308
347
|
const metadata = JSON.parse(command('npm', ['view', `${PACKAGE}@${version}`, '--json']));
|
|
309
348
|
if (metadata.version !== version || !metadata.dist?.tarball) {
|
|
@@ -340,8 +379,12 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
340
379
|
if (sha256(npm.path) !== identity.packageSha256 || sha256(bundle.path) !== identity.bundleSha256
|
|
341
380
|
|| npm.version !== identity.version || bundle.sha !== identity.candidateSha
|
|
342
381
|
|| signature.sha !== identity.candidateSha) throw new Error('nightly public artifact identity mismatch');
|
|
382
|
+
const timeoutMs = Number(process.env.RUVNET_PUBLIC_NATIVE_TIMEOUT_MS || 10 * 60 * 1000);
|
|
383
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 60_000) {
|
|
384
|
+
throw new Error('RUVNET_PUBLIC_NATIVE_TIMEOUT_MS must be an integer of at least 60000ms');
|
|
385
|
+
}
|
|
343
386
|
const proof = await runNightlyTwoRunProof({ packagePath: npm.path, bundlePath: bundle.path,
|
|
344
|
-
signaturePath: signature.path, sourceSha: identity.candidateSha, workflowRunId,
|
|
387
|
+
signaturePath: signature.path, sourceSha: identity.candidateSha, workflowRunId, timeoutMs,
|
|
345
388
|
out: path.join(temp, 'nightly-proof.json') });
|
|
346
389
|
const validation = validateNightlyProofReceipt(proof, { platform: process.platform, version: identity.version,
|
|
347
390
|
packageSha256: identity.packageSha256, bundleSha256: identity.bundleSha256,
|
|
@@ -355,6 +398,44 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
355
398
|
}
|
|
356
399
|
},
|
|
357
400
|
|
|
401
|
+
async runNativeNightlySmoke({ identity, workflowRunId }) {
|
|
402
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-scheduler-smoke-'));
|
|
403
|
+
try {
|
|
404
|
+
const npm = await this.downloadNpm({ version: identity.version,
|
|
405
|
+
destination: path.join(temp, 'package.tgz') });
|
|
406
|
+
const bundle = await this.downloadGithub({ tag: identity.tag, assetName: 'ruvnet-brain.zip',
|
|
407
|
+
destination: path.join(temp, 'ruvnet-brain.zip') });
|
|
408
|
+
if (sha256(npm.path) !== identity.packageSha256 || sha256(bundle.path) !== identity.bundleSha256
|
|
409
|
+
|| npm.version !== identity.version || bundle.sha !== identity.candidateSha) {
|
|
410
|
+
throw new Error('scheduler smoke public artifact identity mismatch');
|
|
411
|
+
}
|
|
412
|
+
const packageRoot = installedPackageRoot || path.join(temp, 'package');
|
|
413
|
+
if (!installedPackageRoot) {
|
|
414
|
+
const extraction = tarExtractionInvocation(npm.path, temp);
|
|
415
|
+
command('tar', extraction.args, { cwd: extraction.cwd });
|
|
416
|
+
if (!fs.existsSync(packageRoot)) throw new Error('scheduler smoke package extraction failed');
|
|
417
|
+
}
|
|
418
|
+
const brainHome = path.join(temp, 'brain');
|
|
419
|
+
const smokeHome = path.join(temp, 'home');
|
|
420
|
+
fs.mkdirSync(smokeHome, { recursive: true });
|
|
421
|
+
const smokeEnv = { ...process.env, HOME: smokeHome, USERPROFILE: smokeHome,
|
|
422
|
+
RUVNET_BRAIN_HOME: brainHome, RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
|
|
423
|
+
PATH: process.env.PATH || '' };
|
|
424
|
+
const smoke = await runNativeSchedulerSmoke({ packageRoot, packagePath: npm.path,
|
|
425
|
+
bundlePath: installedBundlePath || bundle.path, sourceSha: identity.candidateSha,
|
|
426
|
+
workflowRunId, env: smokeEnv, brainHome, kbDir: path.join(brainHome, 'kb'),
|
|
427
|
+
scheduler: await import(`${pathToFileURL(path.join(packageRoot, 'plugin', 'scripts', 'nightly-scheduler.mjs')).href}?smoke=${Date.now()}`) });
|
|
428
|
+
const validation = validateNativeSchedulerSmoke(smoke, {
|
|
429
|
+
platform: process.platform, sourceSha: identity.candidateSha,
|
|
430
|
+
workflowRunId, bundleSha256: identity.bundleSha256, packageSha256: identity.packageSha256,
|
|
431
|
+
});
|
|
432
|
+
if (!validation.ok) throw new Error(`scheduler smoke failed: ${validation.failures.join('; ')}`);
|
|
433
|
+
return smoke;
|
|
434
|
+
} finally {
|
|
435
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
|
|
358
439
|
async installHosts({ artifactPath, artifactSha256, bundlePath, bundleSha256, version }) {
|
|
359
440
|
const temp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-install-')));
|
|
360
441
|
installTemp = temp;
|
|
@@ -362,14 +443,20 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
362
443
|
const extraction = tarExtractionInvocation(artifactPath, temp);
|
|
363
444
|
command('tar', extraction.args, { cwd: extraction.cwd });
|
|
364
445
|
stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot });
|
|
446
|
+
installedPackageRoot = packageRoot;
|
|
447
|
+
installedBundlePath = path.join(packageRoot, 'dist', 'ruvnet-brain.zip');
|
|
365
448
|
const sealedPlugin = path.join(packageRoot, 'plugin');
|
|
366
449
|
const results = {};
|
|
367
450
|
let bundle = null;
|
|
368
451
|
const sharedModelCache = path.join(temp, 'models-cache');
|
|
369
452
|
fs.mkdirSync(sharedModelCache, { recursive: true });
|
|
370
|
-
// Derived from HOST_MODES, so a fourth host shape is added in ONE place and this
|
|
371
|
-
// cannot fall behind the staged-side check the way it did.
|
|
372
|
-
|
|
453
|
+
// Derived from HOST_MODES, so a fourth host shape is added in ONE place and this matrix
|
|
454
|
+
// cannot fall behind the staged-side check the way it did. Each host owns its HOME and
|
|
455
|
+
// mutable state, so installation runs concurrently rather than adding three cold-start
|
|
456
|
+
// durations together on slower runners. Search is deliberately a second phase: one real
|
|
457
|
+
// query warms the shared model cache before the other two hosts start their MCP workers.
|
|
458
|
+
const hostResults = await Promise.all(HOST_MODES.map(async (hostMode) => {
|
|
459
|
+
const mode = RECEIPT_MODE_NAMES[hostMode];
|
|
373
460
|
console.log(`Public verification: installing ${mode} on ${process.platform}`);
|
|
374
461
|
const home = path.join(temp, `home-${mode}`);
|
|
375
462
|
const codexHome = path.join(home, '.codex');
|
|
@@ -398,12 +485,12 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
398
485
|
? { CODEX_BIN: nativeCodexExecutable() } : {}),
|
|
399
486
|
};
|
|
400
487
|
if (mode !== 'claudeOnly') {
|
|
401
|
-
|
|
402
|
-
|
|
488
|
+
await commandAsync(env.CODEX_BIN || 'codex', ['--version'], { env, timeout: 30_000, stdio: 'inherit' });
|
|
489
|
+
await commandAsync(env.CODEX_BIN || 'codex', ['plugin', 'list', '--json'], { env, timeout: 30_000, stdio: 'inherit' });
|
|
403
490
|
}
|
|
404
|
-
if (process.platform === 'win32')
|
|
491
|
+
if (process.platform === 'win32') await commandAsync('git', ['--version'], { env, timeout: 30_000, stdio: 'inherit' });
|
|
405
492
|
const installer = path.join(packageRoot, 'bin', 'install.mjs');
|
|
406
|
-
|
|
493
|
+
await commandAsync(process.execPath, [
|
|
407
494
|
installer, '--yes', '--force', '--version', `v${version}`,
|
|
408
495
|
'--no-nightly-prompt', '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline',
|
|
409
496
|
'--no-selfcheck', '--no-verify',
|
|
@@ -425,21 +512,44 @@ export function livePublicationAdapter({ root = process.cwd(), candidateRoot = r
|
|
|
425
512
|
throw new Error(`${mode} installed public Brain bundle version mismatch`);
|
|
426
513
|
}
|
|
427
514
|
bundle ||= { brainVersion: source.brainVersion, releaseTag: source.releaseTag };
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
515
|
+
return {
|
|
516
|
+
mode,
|
|
517
|
+
verified,
|
|
518
|
+
context: { temp, home, codexHome, brainHome, kb, env, packageRoot },
|
|
519
|
+
installer,
|
|
520
|
+
bundle: { brainVersion: source.brainVersion, releaseTag: source.releaseTag },
|
|
521
|
+
};
|
|
522
|
+
}));
|
|
523
|
+
|
|
524
|
+
// Warm one installed worker before opening the other two. Concurrent first-run model loads
|
|
525
|
+
// contend for the same cache and can exceed the public 30-second search acceptance window;
|
|
526
|
+
// this keeps the window strict while making the expensive work bounded and parallel.
|
|
527
|
+
const searched = new Map();
|
|
528
|
+
const first = hostResults[0];
|
|
529
|
+
searched.set(first.mode, await rpcSearch(findMcpServer(first.context.home), first.context.env,
|
|
530
|
+
'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS));
|
|
531
|
+
await Promise.all(hostResults.slice(1).map(async ({ mode, context }) => {
|
|
532
|
+
searched.set(mode, await rpcSearch(findMcpServer(context.home), context.env,
|
|
533
|
+
'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS));
|
|
534
|
+
}));
|
|
535
|
+
await Promise.all(hostResults.map(async ({ context, installer }) => {
|
|
536
|
+
await commandAsync(process.execPath, [installer, '--doctor', '--hooks'], {
|
|
537
|
+
env: context.env, cwd: packageRoot, timeout: 300_000, stdio: 'inherit',
|
|
432
538
|
});
|
|
539
|
+
}));
|
|
540
|
+
|
|
541
|
+
for (const { mode, verified, context } of hostResults) {
|
|
542
|
+
const search = searched.get(mode);
|
|
433
543
|
results[mode] = {
|
|
434
544
|
status: 'PASS', doctorExit: 0, version, artifactSha256,
|
|
435
|
-
functionalSearch: true, searchMs:
|
|
545
|
+
functionalSearch: true, searchMs: search.broadMs, hostsOnPath: mode,
|
|
436
546
|
...verified,
|
|
437
547
|
};
|
|
438
|
-
installContexts.set(MODE_FROM_RECEIPT_NAME[mode],
|
|
548
|
+
installContexts.set(MODE_FROM_RECEIPT_NAME[mode], context);
|
|
439
549
|
}
|
|
440
550
|
return {
|
|
441
551
|
...results,
|
|
442
|
-
bundle,
|
|
552
|
+
bundle: hostResults[0]?.bundle || bundle,
|
|
443
553
|
};
|
|
444
554
|
},
|
|
445
555
|
|
|
@@ -30,6 +30,17 @@ const LANES = Object.freeze({
|
|
|
30
30
|
{ kind: 'command', command: npm, args: ['run', 'version:check'], timeoutMs: 60_000 },
|
|
31
31
|
vitest(['tests/qe/gpt56/critical-risk-map.test.mjs'], 60_000),
|
|
32
32
|
],
|
|
33
|
+
// Early public-artifact smoke: exercise the exact packed installer and host convergence on
|
|
34
|
+
// every OS before publication. This is deliberately smaller than terminal public verification:
|
|
35
|
+
// it proves the candidate package boundary early, while the protected post-publication matrix
|
|
36
|
+
// still owns public bytes, retrieval canaries, and native scheduled-update proof.
|
|
37
|
+
'early-public': [
|
|
38
|
+
vitest([
|
|
39
|
+
'tests/qe/release/packed-clean-install.test.mjs',
|
|
40
|
+
'tests/qe/release/issue-64-host-convergence.test.mjs',
|
|
41
|
+
'tests/unit/npm-tarball-codex.test.mjs',
|
|
42
|
+
], 180_000),
|
|
43
|
+
],
|
|
33
44
|
check: [
|
|
34
45
|
vitest([
|
|
35
46
|
'tests/qe/gpt56/critical-risk-map.test.mjs',
|
|
@@ -202,6 +202,12 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
202
202
|
? receiptsFor(hydratedRelease(releaseById(matchingDrafts[0].id))) : [];
|
|
203
203
|
const latestByTransaction = new Map();
|
|
204
204
|
for (const release of all) {
|
|
205
|
+
// Draft-release assets are not readable through the public GitHub API (they return 404
|
|
206
|
+
// even though the release metadata is visible). The matching candidate above is read
|
|
207
|
+
// explicitly because the publisher owns it; unrelated drafts are not public channel
|
|
208
|
+
// state and must not make every subsequent release fail while scanning history. Published
|
|
209
|
+
// releases remain fully receipt-gated and continue to block competing transactions.
|
|
210
|
+
if (release.draft && release.tag_name !== identity.tag) continue;
|
|
205
211
|
for (const receipt of receiptsFor(hydratedRelease(release))) {
|
|
206
212
|
const prior = latestByTransaction.get(receipt.transactionId);
|
|
207
213
|
if (!prior || receipt.sequence > prior.receipt.sequence) {
|