ruvnet-brain 4.3.1 → 4.3.8
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 +12 -13
- package/bin/install.mjs +18 -8
- package/console/app.js +4 -4
- package/data/model-catalog.json +3 -3
- package/kb/verify-citation.mjs +32 -5
- package/package.json +13 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +4 -2
- package/plugin/scripts/codex-hook-adapter.mjs +12 -7
- package/plugin/scripts/codex-hook-events.mjs +39 -0
- package/plugin/scripts/coverage-integrity.mjs +4 -4
- package/plugin/scripts/hook-shim.mjs +9 -7
- package/plugin/scripts/learn-capture.sh +20 -1
- package/plugin/scripts/learn-flush.mjs +7 -1
- package/plugin/scripts/lesson-gate.mjs +17 -2
- package/plugin/scripts/lesson-hooks.sh +17 -2
- package/plugin/scripts/session-start-core.mjs +47 -11
- package/plugin/skills/release-proof/SKILL.md +19 -10
- package/plugin/skills/release-proof/references/receipt-contract.md +18 -8
- package/scripts/brain-stamp-resolve.mjs +22 -0
- package/scripts/brain-stamp.mjs +5 -3
- package/scripts/build-bundle.mjs +41 -4
- package/scripts/candidate-host-evidence.mjs +9 -2
- package/scripts/ci/step-watchdog.mjs +86 -0
- package/scripts/convergence-manifest.mjs +66 -0
- package/scripts/distill-project.mjs +22 -4
- package/scripts/eval-brain.mjs +13 -3
- package/scripts/execution-policy.mjs +111 -0
- package/scripts/execution-preflight.mjs +18 -0
- package/scripts/git-hooks/pre-push +16 -0
- package/scripts/health-repair.mjs +5 -1
- package/scripts/host-install-matrix.mjs +247 -40
- package/scripts/integration-evidence.mjs +73 -0
- package/scripts/nightly-wrapper.sh +4 -3
- package/scripts/onboarding-console.mjs +3 -3
- package/scripts/prepublication-evidence.mjs +174 -0
- package/scripts/protected-release-invocation.mjs +32 -4
- package/scripts/public-verification-aggregate.mjs +27 -54
- package/scripts/public-verification-inputs.mjs +5 -3
- package/scripts/qa-runner.mjs +72 -0
- package/scripts/qe/agentic-qe-4.3.mjs +195 -0
- package/scripts/qe/aggregate-4.3.mjs +42 -0
- package/scripts/record-lesson.mjs +28 -6
- package/scripts/refresh-model-catalog.mjs +32 -3
- package/scripts/release-authority.mjs +19 -7
- package/scripts/release-convergence-watchdog.mjs +10 -22
- package/scripts/release-projection.mjs +76 -32
- package/scripts/release-transaction.mjs +1 -1
- package/scripts/release.mjs +9 -15
- package/scripts/retrieval-canary.mjs +14 -6
- package/scripts/set-version.mjs +31 -0
- package/scripts/source-coverage.mjs +11 -2
- package/scripts/staged-host-verifier.mjs +2 -2
- package/scripts/verify-channels.mjs +1 -1
- package/scripts/wired-check.mjs +1 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One deterministic boundary between Brain knowledge and consequential execution.
|
|
3
|
+
// This module is deliberately pure: it never loads credentials, calls a provider, or mutates state.
|
|
4
|
+
|
|
5
|
+
const NATIVE_HOSTS = new Set(['claude', 'codex']);
|
|
6
|
+
const API_EXECUTORS = new Set(['agent_execute', 'sdk', 'openrouter', 'api']);
|
|
7
|
+
const ARCHITECTURE_TERMS = /\b(adr|ddd|architecture|release|deploy|qa|security|schema|migration)\b/i;
|
|
8
|
+
const CONSEQUENTIAL_ACTIONS = new Set(['delegate', 'write', 'release', 'external']);
|
|
9
|
+
const FRESHNESS_MS = 30 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
const hex64 = (value) => /^[a-f0-9]{64}$/i.test(String(value || ''));
|
|
12
|
+
const fresh = (value, now = Date.now()) => {
|
|
13
|
+
const time = Date.parse(String(value || ''));
|
|
14
|
+
return Number.isFinite(time) && time <= now && now - time <= FRESHNESS_MS;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The classifier used to trust caller-supplied routing fields. That made a correct memory lesson
|
|
19
|
+
* advisory: a caller could omit the live search and AgentDB read and still receive ALLOW. This is
|
|
20
|
+
* the evidence boundary used by the executable preflight. Receipts are intentionally structural;
|
|
21
|
+
* the producers (search_ruvnet and ruflo memory retrieve) own their contents and identities.
|
|
22
|
+
*/
|
|
23
|
+
export function validateEvidence(input = {}, now = Date.now()) {
|
|
24
|
+
const failures = [];
|
|
25
|
+
const grounding = input.groundingReceipt;
|
|
26
|
+
const memory = input.memoryReceipt;
|
|
27
|
+
if (!grounding || grounding.status !== 'success' || !fresh(grounding.observedAt, now)) {
|
|
28
|
+
failures.push('fresh successful search_ruvnet receipt is required');
|
|
29
|
+
} else if (!(Array.isArray(grounding.sources) && grounding.sources.length > 0)
|
|
30
|
+
&& !hex64(grounding.sourceIdentity)) {
|
|
31
|
+
failures.push('grounding receipt must bind a source list or source identity');
|
|
32
|
+
}
|
|
33
|
+
if (!memory || memory.status !== 'retrieved' || !fresh(memory.observedAt, now)) {
|
|
34
|
+
failures.push('fresh exact AgentDB checkpoint retrieval is required');
|
|
35
|
+
} else {
|
|
36
|
+
if (!String(memory.path || '').endsWith('/.swarm/memory.db')) failures.push('memory receipt must use the project .swarm/memory.db');
|
|
37
|
+
if (!/^project-state-current-\d+$/.test(String(memory.key || ''))) failures.push('memory receipt must retrieve an append-only project-state-current key');
|
|
38
|
+
if (!hex64(memory.valueDigest)) failures.push('memory receipt must bind the retrieved value digest');
|
|
39
|
+
}
|
|
40
|
+
return { valid: failures.length === 0, failures };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function classifyExecutionPolicy(input = {}) {
|
|
44
|
+
const action = String(input.action || 'read');
|
|
45
|
+
const description = String(input.description || '');
|
|
46
|
+
const changedFiles = [...new Set((input.changedFiles || []).map(String).filter(Boolean))];
|
|
47
|
+
const nativeHosts = [...new Set((input.nativeHosts || []).map(String).filter((h) => NATIVE_HOSTS.has(h)))];
|
|
48
|
+
const requestedExecutor = String(input.requestedExecutor || '');
|
|
49
|
+
const explicitSwarm = input.explicitSwarm === true;
|
|
50
|
+
const multiFile = changedFiles.length >= 3;
|
|
51
|
+
const architectureTask = ARCHITECTURE_TERMS.test(description) || ['release', 'external'].includes(action);
|
|
52
|
+
const swarmRequired = explicitSwarm || multiFile || architectureTask;
|
|
53
|
+
const swarmReason = explicitSwarm
|
|
54
|
+
? 'explicit-swarm-request'
|
|
55
|
+
: multiFile
|
|
56
|
+
? 'three-or-more-independent-file-surfaces'
|
|
57
|
+
: architectureTask
|
|
58
|
+
? 'architecture-or-consequential-action'
|
|
59
|
+
: 'single-sequential-surface';
|
|
60
|
+
|
|
61
|
+
const evidence = CONSEQUENTIAL_ACTIONS.has(action) && input.enforceEvidence === true
|
|
62
|
+
? validateEvidence(input, input.now ?? Date.now())
|
|
63
|
+
: { valid: true, failures: [] };
|
|
64
|
+
if (!evidence.valid) {
|
|
65
|
+
return {
|
|
66
|
+
schema: 'ruvnet-brain.execution-policy.v1',
|
|
67
|
+
verdict: 'REFUSE', action, swarmRequired, swarmReason,
|
|
68
|
+
executor: 'unknown', reason: 'live-evidence-preflight-failed', evidence,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (action !== 'delegate') {
|
|
73
|
+
return {
|
|
74
|
+
schema: 'ruvnet-brain.execution-policy.v1',
|
|
75
|
+
verdict: 'ALLOW', action, swarmRequired, swarmReason, evidence,
|
|
76
|
+
executor: 'current-agent', reason: 'delegation-not-requested',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const nativeHost = NATIVE_HOSTS.has(input.activeHost) && nativeHosts.includes(input.activeHost)
|
|
81
|
+
? input.activeHost : nativeHosts[0] || null;
|
|
82
|
+
if (API_EXECUTORS.has(requestedExecutor)) {
|
|
83
|
+
return {
|
|
84
|
+
schema: 'ruvnet-brain.execution-policy.v1',
|
|
85
|
+
verdict: nativeHost ? 'REFUSE' : 'DEGRADED', action, swarmRequired, swarmReason, evidence,
|
|
86
|
+
executor: nativeHost ? `native:${nativeHost}` : 'unknown',
|
|
87
|
+
reason: nativeHost
|
|
88
|
+
? 'api-backed-executor-is-not-the-native-subscription-route'
|
|
89
|
+
: 'no-authenticated-native-host-is-available',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (!nativeHost) {
|
|
93
|
+
return {
|
|
94
|
+
schema: 'ruvnet-brain.execution-policy.v1',
|
|
95
|
+
verdict: 'DEGRADED', action, swarmRequired, swarmReason, executor: 'unknown', evidence,
|
|
96
|
+
reason: 'no-authenticated-native-host-is-available',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
schema: 'ruvnet-brain.execution-policy.v1',
|
|
101
|
+
verdict: 'ALLOW', action, swarmRequired, swarmReason, evidence,
|
|
102
|
+
executor: `native:${nativeHost}`, reason: 'native-subscription-route-selected',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
107
|
+
const raw = process.argv[2] || '{}';
|
|
108
|
+
let input;
|
|
109
|
+
try { input = JSON.parse(raw); } catch { process.stderr.write('execution-policy: input must be JSON\n'); process.exit(2); }
|
|
110
|
+
process.stdout.write(`${JSON.stringify(classifyExecutionPolicy(input))}\n`);
|
|
111
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Enforced knowledge-to-execution boundary. The caller must provide receipts produced by the
|
|
3
|
+
// live Brain search and exact project AgentDB retrieval; prose, memory, and guessed host state do
|
|
4
|
+
// not satisfy this command.
|
|
5
|
+
import { classifyExecutionPolicy } from './execution-policy.mjs';
|
|
6
|
+
|
|
7
|
+
export function runExecutionPreflight(input = {}) {
|
|
8
|
+
return classifyExecutionPolicy({ ...input, enforceEvidence: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
12
|
+
let input;
|
|
13
|
+
try { input = JSON.parse(process.argv[2] || '{}'); }
|
|
14
|
+
catch { process.stderr.write('execution-preflight: input must be JSON\n'); process.exit(2); }
|
|
15
|
+
const result = runExecutionPreflight(input);
|
|
16
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
17
|
+
process.exit(result.verdict === 'ALLOW' ? 0 : 2);
|
|
18
|
+
}
|
|
@@ -114,6 +114,22 @@ if [ "$commands_status" -ne 0 ]; then
|
|
|
114
114
|
exit 1
|
|
115
115
|
fi
|
|
116
116
|
|
|
117
|
+
# The convergence manifest is the exact source/version/ADR identity consumed by hosted QA.
|
|
118
|
+
# Check it here so a push cannot publish a commit whose generated identity was produced before
|
|
119
|
+
# the final source edit (the failure mode that repeatedly escaped local pre-push validation).
|
|
120
|
+
if [ -f "$ROOT/scripts/convergence-manifest.mjs" ]; then
|
|
121
|
+
node "$ROOT/scripts/convergence-manifest.mjs"
|
|
122
|
+
convergence_status=$?
|
|
123
|
+
else
|
|
124
|
+
convergence_status=0
|
|
125
|
+
fi
|
|
126
|
+
if [ "$convergence_status" -ne 0 ]; then
|
|
127
|
+
echo ""
|
|
128
|
+
echo " ⛔ pre-push gate refused this push — convergence identity is stale."
|
|
129
|
+
echo " Run: npm run convergence:write"
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
117
133
|
# ── DOCUMENT CURRENCY (ADR-056 §5) ─────────────────────────────────────────────────────────────────
|
|
118
134
|
# The tool was written and tested on 2026-07-22 and then invoked by NOTHING for five days — absent
|
|
119
135
|
# from this hook, gate.sh, gates.mjs and every workflow, while `wired-check` reported it "wired"
|
|
@@ -25,11 +25,15 @@ import os from 'node:os';
|
|
|
25
25
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { findStores, diagnose } from './memory-doctor.mjs';
|
|
27
27
|
import { learnerCwd, loadRuntimePreferences } from '../plugin/scripts/runtime-preferences.mjs';
|
|
28
|
+
import { projectDirectory } from '../plugin/scripts/project-identity.mjs';
|
|
28
29
|
|
|
29
30
|
const HOME = os.homedir();
|
|
30
31
|
// The SAME project root learn-flush.mjs computes, by the same rule — the two halves of the flush
|
|
31
32
|
// have to agree about which project they mean or they address different queues (issue #104).
|
|
32
|
-
|
|
33
|
+
// RESIDUAL of #134: RUVNET_BRAIN_PROJECT_DIR is never set by real hook dispatch on either host, so
|
|
34
|
+
// consult `projectDirectory()` (project-identity.mjs) — the CLAUDE_PROJECT_DIR-with-containment
|
|
35
|
+
// rule #85/#107 already established, reused rather than trusting the variable unconditionally.
|
|
36
|
+
const PROJECT = process.env.RUVNET_BRAIN_PROJECT_DIR || projectDirectory();
|
|
33
37
|
const argv = process.argv.slice(2);
|
|
34
38
|
const has = (f) => argv.includes(f);
|
|
35
39
|
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
import fs from 'node:fs';
|
|
26
26
|
import os from 'node:os';
|
|
27
27
|
import path from 'node:path';
|
|
28
|
-
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
29
|
+
import { pathToFileURL } from 'node:url';
|
|
29
30
|
|
|
30
31
|
/** The three host shapes a release must survive. ONE name each, for every consumer. */
|
|
31
32
|
export const HOST_MODES = Object.freeze(['claude', 'codex', 'dual']);
|
|
@@ -58,7 +59,7 @@ export const MODE_FROM_RECEIPT_NAME = Object.freeze({ claudeOnly: 'claude', code
|
|
|
58
59
|
export const VARIANTS = Object.freeze({
|
|
59
60
|
staged: Object.freeze({
|
|
60
61
|
installerArgs: (_v) => ['--local', '--yes', '--force', '--no-nightly-prompt',
|
|
61
|
-
'--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline', '--no-selfcheck'],
|
|
62
|
+
'--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline', '--no-selfcheck', '--no-verify'],
|
|
62
63
|
env: ({ packageRoot }) => ({
|
|
63
64
|
RUVNET_CLAUDE_MARKETPLACE_SOURCE: packageRoot,
|
|
64
65
|
RUVNET_CODEX_HOOK_TRUST_MODE: 'bypass',
|
|
@@ -131,46 +132,252 @@ export function runHostMatrix({ packageRoot, version, variant = 'staged', locate
|
|
|
131
132
|
let error;
|
|
132
133
|
|
|
133
134
|
for (const mode of HOST_MODES) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const codexHome = path.join(home, '.codex');
|
|
137
|
-
const brainHome = path.join(home, '.cache', 'ruvnet-brain');
|
|
138
|
-
fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
|
|
139
|
-
if (mode !== 'claude') fs.mkdirSync(codexHome, { recursive: true });
|
|
140
|
-
const env = {
|
|
141
|
-
...process.env,
|
|
142
|
-
HOME: home,
|
|
143
|
-
CODEX_HOME: codexHome,
|
|
144
|
-
RUVNET_BRAIN_HOME: brainHome,
|
|
145
|
-
RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
|
|
146
|
-
CI: 'true',
|
|
147
|
-
PATH: fixturePath(mode, workspace, locate),
|
|
148
|
-
...spec.env({ packageRoot }),
|
|
149
|
-
};
|
|
150
|
-
const install = run(process.execPath, [installer, ...spec.installerArgs(version)], {
|
|
151
|
-
cwd: packageRoot, env, encoding: 'utf8', timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024,
|
|
152
|
-
});
|
|
153
|
-
if (install.error || install.status !== 0) {
|
|
154
|
-
throw new Error(`install failed for ${mode}: ${(install.stderr || install.error?.message || '').slice(-4000)}`);
|
|
155
|
-
}
|
|
156
|
-
const doctor = run(process.execPath, [installer, '--doctor', '--hooks'], {
|
|
157
|
-
cwd: packageRoot, env, encoding: 'utf8', timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
|
|
158
|
-
});
|
|
159
|
-
const classified = classifyDoctor(doctor);
|
|
160
|
-
fixtures[mode] = { status: classified.status, doctorExit: doctor.status, version };
|
|
161
|
-
if (!classified.accepted) {
|
|
162
|
-
verdict = 'FAIL';
|
|
163
|
-
fixtures[mode].output = classified.output.slice(-5000);
|
|
164
|
-
// The output belongs IN the error. The previous harness put it there and I dropped it in
|
|
165
|
-
// the consolidation, so CI reported a bare "doctor failed for claude" and the one thing
|
|
166
|
-
// needed to act on it — what the doctor actually said — was thrown away.
|
|
167
|
-
error = error || `doctor failed for ${mode} (exit ${doctor.status}): ${classified.output.slice(-4000)}`;
|
|
168
|
-
}
|
|
169
|
-
} catch (e) {
|
|
135
|
+
const result = runHostMode({ mode, packageRoot, version, spec, workspace, locate, run });
|
|
136
|
+
if (result.error) {
|
|
170
137
|
verdict = 'FAIL';
|
|
171
|
-
fixtures[mode] =
|
|
172
|
-
error = error ||
|
|
138
|
+
fixtures[mode] = result.fixture;
|
|
139
|
+
error = error || result.error;
|
|
140
|
+
} else {
|
|
141
|
+
fixtures[mode] = result.fixture;
|
|
173
142
|
}
|
|
174
143
|
}
|
|
175
144
|
return error ? { verdict, fixtures, error } : { verdict, fixtures };
|
|
176
145
|
}
|
|
146
|
+
|
|
147
|
+
function runHostMode({ mode, packageRoot, version, spec, workspace, locate, run }) {
|
|
148
|
+
try {
|
|
149
|
+
const home = path.join(workspace, `home-${mode}`);
|
|
150
|
+
const codexHome = path.join(home, '.codex');
|
|
151
|
+
const brainHome = path.join(home, '.cache', 'ruvnet-brain');
|
|
152
|
+
fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
|
|
153
|
+
if (mode !== 'claude') fs.mkdirSync(codexHome, { recursive: true });
|
|
154
|
+
const env = {
|
|
155
|
+
...process.env,
|
|
156
|
+
HOME: home,
|
|
157
|
+
CODEX_HOME: codexHome,
|
|
158
|
+
RUVNET_BRAIN_HOME: brainHome,
|
|
159
|
+
RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
|
|
160
|
+
CI: 'true',
|
|
161
|
+
PATH: fixturePath(mode, workspace, locate),
|
|
162
|
+
...spec.env({ packageRoot }),
|
|
163
|
+
};
|
|
164
|
+
const installer = path.join(packageRoot, 'bin', 'install.mjs');
|
|
165
|
+
const install = run(process.execPath, [installer, ...spec.installerArgs(version)], {
|
|
166
|
+
cwd: packageRoot, env, encoding: 'utf8', timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024,
|
|
167
|
+
});
|
|
168
|
+
if (install.error || install.status !== 0) {
|
|
169
|
+
throw new Error(`install failed for ${mode}: ${(install.stderr || install.error?.message || '').slice(-4000)}`);
|
|
170
|
+
}
|
|
171
|
+
const doctor = run(process.execPath, [installer, '--doctor', '--hooks'], {
|
|
172
|
+
cwd: packageRoot, env, encoding: 'utf8', timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
|
|
173
|
+
});
|
|
174
|
+
const classified = classifyDoctor(doctor);
|
|
175
|
+
const fixture = { status: classified.status, doctorExit: doctor.status, version };
|
|
176
|
+
if (!classified.accepted) {
|
|
177
|
+
fixture.output = classified.output.slice(-5000);
|
|
178
|
+
return {
|
|
179
|
+
fixture,
|
|
180
|
+
error: `doctor failed for ${mode} (exit ${doctor.status}): ${classified.output.slice(-4000)}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return { fixture };
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return { fixture: { status: 'FAIL', error: e.message }, error: e.message };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Async equivalent used by hosted release qualification. Each mode owns its HOME and PATH, so
|
|
191
|
+
* the expensive installer/doctor pairs can run concurrently without sharing mutable state.
|
|
192
|
+
* Results are reassembled in HOST_MODES order before the single caller writes its receipt.
|
|
193
|
+
*/
|
|
194
|
+
export async function runHostMatrixAsync({
|
|
195
|
+
packageRoot,
|
|
196
|
+
version,
|
|
197
|
+
variant = 'staged',
|
|
198
|
+
locate,
|
|
199
|
+
temp,
|
|
200
|
+
runCommand = spawnCommand,
|
|
201
|
+
runMcpSearch = runInstalledMcpSearch,
|
|
202
|
+
verifyGrounding = verifyInstalledGrounding,
|
|
203
|
+
resolveMcpServer = resolveInstalledMcpServer,
|
|
204
|
+
}) {
|
|
205
|
+
const spec = VARIANTS[variant];
|
|
206
|
+
if (!spec) throw new Error(`unknown host-matrix variant: ${variant}`);
|
|
207
|
+
const workspace = temp || fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-host-matrix-'));
|
|
208
|
+
const sharedModelCache = path.join(workspace, 'models-cache');
|
|
209
|
+
fs.mkdirSync(sharedModelCache, { recursive: true });
|
|
210
|
+
const contexts = HOST_MODES.map((mode) => {
|
|
211
|
+
const home = path.join(workspace, `home-${mode}`);
|
|
212
|
+
const codexHome = path.join(home, '.codex');
|
|
213
|
+
const brainHome = path.join(home, '.cache', 'ruvnet-brain');
|
|
214
|
+
fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
|
|
215
|
+
if (mode !== 'claude') fs.mkdirSync(codexHome, { recursive: true });
|
|
216
|
+
const env = {
|
|
217
|
+
...process.env,
|
|
218
|
+
HOME: home,
|
|
219
|
+
CODEX_HOME: codexHome,
|
|
220
|
+
RUVNET_BRAIN_HOME: brainHome,
|
|
221
|
+
RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
|
|
222
|
+
KB_MODEL_CACHE: sharedModelCache,
|
|
223
|
+
CI: 'true',
|
|
224
|
+
PATH: fixturePath(mode, workspace, locate),
|
|
225
|
+
...spec.env({ packageRoot }),
|
|
226
|
+
};
|
|
227
|
+
return { mode, home, env };
|
|
228
|
+
});
|
|
229
|
+
const installer = path.join(packageRoot, 'bin', 'install.mjs');
|
|
230
|
+
const installs = await Promise.all(contexts.map(async (context) => {
|
|
231
|
+
const processResult = await runCommand(process.execPath, [installer, ...spec.installerArgs(version)], {
|
|
232
|
+
cwd: packageRoot, env: context.env, timeout: 1_200_000,
|
|
233
|
+
});
|
|
234
|
+
return { context, processResult };
|
|
235
|
+
}));
|
|
236
|
+
const failedInstall = installs.find(({ processResult }) => processResult.error || processResult.status !== 0);
|
|
237
|
+
if (failedInstall) {
|
|
238
|
+
const detail = processDiagnostic(failedInstall.processResult);
|
|
239
|
+
return {
|
|
240
|
+
verdict: 'FAIL',
|
|
241
|
+
fixtures: Object.fromEntries(contexts.map(({ mode }) => [mode, {
|
|
242
|
+
status: mode === failedInstall.context.mode ? 'FAIL' : 'NOT_PROBED',
|
|
243
|
+
...(mode === failedInstall.context.mode ? { process: processIdentity(failedInstall.processResult) } : {}),
|
|
244
|
+
}])),
|
|
245
|
+
error: `install failed for ${failedInstall.context.mode} (${detail})`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const prewarmContext = contexts[0];
|
|
250
|
+
const prewarmReader = path.join(prewarmContext.env.RUVNET_BRAIN_KB, 'forge-ask-all.mjs');
|
|
251
|
+
const prewarm = await runCommand(process.execPath, [prewarmReader, '--dir', prewarmContext.env.RUVNET_BRAIN_KB,
|
|
252
|
+
'--q', 'How does RuvNet Brain prove a public release artifact?', '--k', '1'], {
|
|
253
|
+
cwd: prewarmContext.env.RUVNET_BRAIN_KB, env: prewarmContext.env, timeout: 300_000,
|
|
254
|
+
});
|
|
255
|
+
if (prewarm.error || prewarm.status !== 0) {
|
|
256
|
+
return { verdict: 'FAIL', fixtures: {}, error: `shared model prewarm failed (${processDiagnostic(prewarm)})` };
|
|
257
|
+
}
|
|
258
|
+
const prewarmGrounding = await verifyGrounding(String(prewarm.stdout || ''), prewarmContext.env.RUVNET_BRAIN_KB);
|
|
259
|
+
if (!prewarmGrounding?.grounded) {
|
|
260
|
+
return { verdict: 'FAIL', fixtures: {}, error: 'shared model prewarm returned no grounded source receipt' };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const searches = await Promise.all(contexts.map(async (context) => {
|
|
264
|
+
const serverPath = resolveMcpServer(context);
|
|
265
|
+
const processResult = await runMcpSearch({ mode: context.mode, serverPath, env: context.env });
|
|
266
|
+
const output = `${processResult.stdout || ''}${processResult.stderr || ''}`;
|
|
267
|
+
if (processResult.error || processResult.status !== 0) {
|
|
268
|
+
return { context, processResult, error: `MCP search failed for ${context.mode} (${processDiagnostic(processResult)}): ${output.slice(-2000)}` };
|
|
269
|
+
}
|
|
270
|
+
const grounding = await verifyGrounding(output, context.env.RUVNET_BRAIN_KB);
|
|
271
|
+
if (!grounding?.grounded) return { context, processResult, error: `MCP search grounding unproven for ${context.mode}` };
|
|
272
|
+
return { context, processResult, grounding };
|
|
273
|
+
}));
|
|
274
|
+
const fixtures = Object.fromEntries(searches.map(({ context, processResult, grounding, error }) => [context.mode, {
|
|
275
|
+
status: error ? 'FAIL' : 'PASS',
|
|
276
|
+
version,
|
|
277
|
+
process: processIdentity(processResult),
|
|
278
|
+
...(grounding?.receipt ? { grounding: grounding.receipt } : {}),
|
|
279
|
+
...(error ? { error } : {}),
|
|
280
|
+
}]));
|
|
281
|
+
const error = searches.find((result) => result.error)?.error;
|
|
282
|
+
return error ? { verdict: 'FAIL', fixtures, error } : { verdict: 'PASS', fixtures };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function resolveInstalledMcpServer({ mode, home }) {
|
|
286
|
+
if (mode !== 'claude') return path.join(home, '.claude', 'ruvnet-brain', 'mcp', 'server.mjs');
|
|
287
|
+
const registry = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
|
|
288
|
+
const rows = JSON.parse(fs.readFileSync(registry, 'utf8'))?.plugins?.['ruvnet-brain@ruvnet-brain'];
|
|
289
|
+
const installPath = Array.isArray(rows) ? rows.find(({ scope }) => scope === 'user')?.installPath : null;
|
|
290
|
+
if (!installPath) throw new Error('Claude fixture has no user-scoped ruvnet-brain plugin install');
|
|
291
|
+
const managedRoot = path.resolve(home, '.claude', 'plugins', 'cache', 'ruvnet-brain', 'ruvnet-brain');
|
|
292
|
+
const resolved = path.resolve(installPath);
|
|
293
|
+
if (resolved !== managedRoot && !resolved.startsWith(`${managedRoot}${path.sep}`)) {
|
|
294
|
+
throw new Error('Claude fixture plugin install path escapes its managed cache');
|
|
295
|
+
}
|
|
296
|
+
const server = path.join(resolved, 'mcp', 'server.mjs');
|
|
297
|
+
if (!fs.existsSync(server)) throw new Error(`Claude fixture MCP server missing from installed plugin: ${server}`);
|
|
298
|
+
return server;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function processIdentity(result) {
|
|
302
|
+
return {
|
|
303
|
+
status: result.status ?? null,
|
|
304
|
+
signal: result.signal ?? null,
|
|
305
|
+
errorCode: result.error?.code ?? null,
|
|
306
|
+
errorMessage: result.error?.message ?? null,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function processDiagnostic(result) {
|
|
311
|
+
const process = processIdentity(result);
|
|
312
|
+
return `status=${process.status} signal=${process.signal || 'none'} error=${process.errorCode ? `${process.errorCode}: ${process.errorMessage}` : 'none'}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function verifyInstalledGrounding(output, kbDir) {
|
|
316
|
+
const verifier = path.join(kbDir, 'verify-citation.mjs');
|
|
317
|
+
const { verifyGrounding } = await import(pathToFileURL(verifier).href);
|
|
318
|
+
return verifyGrounding(output, kbDir);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function runInstalledMcpSearch({ serverPath, env, query = 'How does RuvNet Brain prove a public release artifact?', timeout = 300_000 }) {
|
|
322
|
+
return new Promise((resolve) => {
|
|
323
|
+
const child = spawn(process.execPath, [serverPath], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
324
|
+
let stdout = '';
|
|
325
|
+
let stderr = '';
|
|
326
|
+
const pending = new Map();
|
|
327
|
+
let settled = false;
|
|
328
|
+
const finish = (result) => {
|
|
329
|
+
if (settled) return;
|
|
330
|
+
settled = true;
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
child.kill('SIGTERM');
|
|
333
|
+
resolve({ status: result.status ?? null, signal: result.signal ?? null, error: result.error ?? null, stdout, stderr });
|
|
334
|
+
};
|
|
335
|
+
const timer = setTimeout(() => finish({ status: null, signal: 'SIGKILL', error: Object.assign(new Error('MCP search timed out'), { code: 'ETIMEDOUT' }) }), timeout);
|
|
336
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
337
|
+
child.stdout.on('data', (chunk) => {
|
|
338
|
+
stdout += chunk;
|
|
339
|
+
for (;;) {
|
|
340
|
+
const newline = stdout.indexOf('\n');
|
|
341
|
+
if (newline < 0) break;
|
|
342
|
+
const line = stdout.slice(0, newline).trim();
|
|
343
|
+
stdout = stdout.slice(newline + 1);
|
|
344
|
+
if (!line) continue;
|
|
345
|
+
let message;
|
|
346
|
+
try { message = JSON.parse(line); } catch { continue; }
|
|
347
|
+
const handler = pending.get(message.id);
|
|
348
|
+
if (handler) { pending.delete(message.id); handler(message); }
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
child.on('error', (error) => finish({ status: null, error }));
|
|
352
|
+
child.on('exit', (status, signal) => {
|
|
353
|
+
if (!settled) finish({ status, signal, ...(status === 0 ? {} : { error: new Error(`MCP server exited ${status}`) }) });
|
|
354
|
+
});
|
|
355
|
+
const call = (id, method, params = {}) => new Promise((done) => {
|
|
356
|
+
pending.set(id, done);
|
|
357
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
358
|
+
});
|
|
359
|
+
(async () => {
|
|
360
|
+
const initialized = await call(1, 'initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'release-host-matrix', version: '1' } });
|
|
361
|
+
if (initialized.error) throw new Error(`MCP initialize failed: ${JSON.stringify(initialized.error)}`);
|
|
362
|
+
const listed = await call(2, 'tools/list');
|
|
363
|
+
if (!listed.result?.tools?.some((tool) => tool.name === 'search_ruvnet')) throw new Error('installed MCP does not advertise search_ruvnet');
|
|
364
|
+
const searched = await call(3, 'tools/call', { name: 'search_ruvnet', arguments: { query, k: 5 } });
|
|
365
|
+
const text = (searched.result?.content || []).map((item) => item.text || '').join('\n');
|
|
366
|
+
if (searched.error || searched.result?.isError) throw new Error(`installed Brain search failed: ${text.slice(0, 400)}`);
|
|
367
|
+
stdout = text;
|
|
368
|
+
finish({ status: 0 });
|
|
369
|
+
})().catch((error) => finish({ status: null, error }));
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function spawnCommand(command, args, options) {
|
|
374
|
+
return new Promise((resolve) => {
|
|
375
|
+
const child = spawn(command, args, options);
|
|
376
|
+
const stdout = [];
|
|
377
|
+
const stderr = [];
|
|
378
|
+
child.stdout?.on('data', (chunk) => stdout.push(chunk));
|
|
379
|
+
child.stderr?.on('data', (chunk) => stderr.push(chunk));
|
|
380
|
+
child.on('close', (status, signal) => resolve({ status, signal, stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString() }));
|
|
381
|
+
child.on('error', (error) => resolve({ status: null, error, stdout: Buffer.concat(stdout).toString(), stderr: Buffer.concat(stderr).toString() }));
|
|
382
|
+
});
|
|
383
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export const EXCLUSION_POLICY = 'release-linux-v1';
|
|
8
|
+
|
|
9
|
+
const ALLOWED_LINUX_SKIP_TITLES = new Set([
|
|
10
|
+
'stores, exact-retrieves, and strictly deduplicates through a temp real AgentDB',
|
|
11
|
+
'exposes self-contained native Console and What is New skills through the real plugin loader',
|
|
12
|
+
'repairs a missing Brain-owned marketplace snapshot before Codex reads plugin state',
|
|
13
|
+
'repairs a malformed Brain-owned marketplace snapshot before Codex reads plugin state',
|
|
14
|
+
'repairs the snapshot without re-enabling an explicitly disabled Codex plugin',
|
|
15
|
+
'reports a marketplace preparation failure without changing Codex configuration',
|
|
16
|
+
'bridges, ranks, and is delivered as additionalContext at the write-code decision point',
|
|
17
|
+
'TEETH: an UNTAGGED row travels no further than the store — the chain is genuinely load-bearing',
|
|
18
|
+
'TEETH: a lesson tagged for a DIFFERENT moment does not fire at this one',
|
|
19
|
+
'TEETH: a value too long to read from search output resolves in FULL via retrieve',
|
|
20
|
+
'a USER-LEVEL store at an explicit --path is reachable the same way',
|
|
21
|
+
'TEETH: a MISSING key reports not-found rather than inventing content',
|
|
22
|
+
'`-y` alone does NOT install the nightly LaunchAgent',
|
|
23
|
+
'`--yes` alone does NOT install the spend-watchdog LaunchAgent',
|
|
24
|
+
'embeds only the changed file on pass two and preserves the unchanged stable ID',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const digestNames = (names) => crypto.createHash('sha256')
|
|
28
|
+
.update(`${JSON.stringify([...names].sort())}\n`)
|
|
29
|
+
.digest('hex');
|
|
30
|
+
|
|
31
|
+
export function buildIntegrationEvidence(report, { sourceSha, runId, runAttempt }) {
|
|
32
|
+
const total = Number(report.numTotalTests || 0);
|
|
33
|
+
const failed = Number(report.numFailedTests || 0) + Number(report.numFailedTestSuites || 0);
|
|
34
|
+
const skipped = Number(report.numPendingTests || 0);
|
|
35
|
+
const todo = Number(report.numTodoTests || 0);
|
|
36
|
+
const passed = Number(report.numPassedTests || 0);
|
|
37
|
+
const assertions = (report.testResults || []).flatMap((suite) => suite.assertionResults || []);
|
|
38
|
+
// Vitest 4 calls skipped assertions `skipped` while older JSON reporters called them
|
|
39
|
+
// `pending`; numPendingTests remains the aggregate count in both schemas.
|
|
40
|
+
const skippedTests = assertions.filter(({ status }) => status === 'skipped' || status === 'pending')
|
|
41
|
+
.map(({ fullName, title }) => ({ fullName: fullName || title, title }))
|
|
42
|
+
.filter(({ fullName, title }) => Boolean(fullName && title));
|
|
43
|
+
const todoTests = assertions.filter(({ status }) => status === 'todo')
|
|
44
|
+
.map(({ fullName, title }) => fullName || title)
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
const unknownSkips = skippedTests.filter(({ title }) => !ALLOWED_LINUX_SKIP_TITLES.has(title));
|
|
47
|
+
if (total <= 0 || failed !== 0 || passed + todo + skipped !== total
|
|
48
|
+
|| skippedTests.length !== skipped || todoTests.length !== todo || unknownSkips.length > 0) {
|
|
49
|
+
const names = unknownSkips.map(({ fullName }) => fullName).join('; ');
|
|
50
|
+
throw new Error(`integration report is not an exact governed pass (${passed} passed, ${todo} todo, ${total} total, ${skipped} skipped; unknown skips: ${names || 'none'})`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: 1, kind: 'ruvnet-brain-integration-evidence', sourceSha,
|
|
54
|
+
workflow: 'integration-linux', runId: Number(runId), runAttempt: Number(runAttempt),
|
|
55
|
+
total, passed, failed: 0, skipped,
|
|
56
|
+
skippedTests: skippedTests.map(({ fullName }) => fullName).sort(),
|
|
57
|
+
todo, todoTests: todoTests.sort(), exclusionPolicy: EXCLUSION_POLICY,
|
|
58
|
+
exclusionsSha256: digestNames([
|
|
59
|
+
...skippedTests.map(({ fullName }) => `skip:${fullName}`),
|
|
60
|
+
...todoTests.map((name) => `todo:${name}`),
|
|
61
|
+
]),
|
|
62
|
+
verdict: 'PASS',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
67
|
+
if (isCli) {
|
|
68
|
+
const value = (name) => process.argv[process.argv.indexOf(name) + 1];
|
|
69
|
+
const receipt = buildIntegrationEvidence(JSON.parse(fs.readFileSync(value('--report'), 'utf8')), {
|
|
70
|
+
sourceSha: value('--sha'), runId: value('--run-id'), runAttempt: value('--run-attempt'),
|
|
71
|
+
});
|
|
72
|
+
fs.writeFileSync(value('--out'), `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx' });
|
|
73
|
+
}
|
|
@@ -199,9 +199,10 @@ sh scripts/memdb-health.sh "$RUVNET_PROJECT_MEMORY_DB" >> "$LOG" 2>&1 \
|
|
|
199
199
|
# maintainer away for a month. So the last step is handed to the nightly, which is already the thing
|
|
200
200
|
# that runs unattended.
|
|
201
201
|
#
|
|
202
|
-
# It does NOT publish
|
|
203
|
-
#
|
|
204
|
-
#
|
|
202
|
+
# It does NOT publish or dispatch a publisher. The watchdog is report-only: release-candidate-
|
|
203
|
+
# preflight runs long qualification once on release/**, then protected-release imports the artifact
|
|
204
|
+
# by exact-SHA name, revalidates it, publishes, executes public 3x3, and appends install-verified.
|
|
205
|
+
# Change-triggered architecture/oracle review is evidence, not publication authority. It stands down on any
|
|
205
206
|
# unexpected state — dirty tree, open PRs, no green exact-SHA evidence, a -dev version, a stalled
|
|
206
207
|
# Actions plane — because a watchdog acting on a partial picture is worse than no watchdog.
|
|
207
208
|
# ── GITHUB HEALTH (2026-08-08). Stuart: "You should never ever let it be in a situation where
|
|
@@ -460,11 +460,11 @@ export function resolveMemoryDb(projectDir) {
|
|
|
460
460
|
})[0];
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
-
function probeMemory(projectDir) {
|
|
463
|
+
function probeMemory(projectDir, { now = Date.now() } = {}) {
|
|
464
464
|
const db = resolveMemoryDb(projectDir);
|
|
465
465
|
const probes = {};
|
|
466
466
|
// compaction survival + session surfacing are filesystem facts, always checkable
|
|
467
|
-
const snapshot = inspectSessionSnapshots(projectDir);
|
|
467
|
+
const snapshot = inspectSessionSnapshots(projectDir, { now });
|
|
468
468
|
const snapshotDetail = snapshot.kind === 'canonical'
|
|
469
469
|
? 'a fresh versioned PreCompact snapshot is present in .swarm/agentdb-sessions.jsonl'
|
|
470
470
|
: snapshot.kind === 'legacy'
|
|
@@ -508,7 +508,7 @@ function probeMemory(projectDir) {
|
|
|
508
508
|
|
|
509
509
|
const cp = robustRead(db, "SELECT max(updated_at) FROM memory_entries WHERE key LIKE 'project-state-current%';");
|
|
510
510
|
if (cp.ok && cp.value) {
|
|
511
|
-
const ageH = (
|
|
511
|
+
const ageH = (now - Number(cp.value) * (String(cp.value).length <= 10 ? 1000 : 1)) / 3.6e6;
|
|
512
512
|
probes.coverage = Number.isFinite(ageH) && ageH < 48
|
|
513
513
|
? { status: 'ok', detail: `project checkpoint present, ~${Math.max(0, ageH).toFixed(0)}h old` }
|
|
514
514
|
: { status: 'warn', detail: 'project checkpoint present but stale (>2 days)' };
|