ruvnet-brain 4.3.1 → 4.3.3

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.
Files changed (38) hide show
  1. package/README.md +12 -13
  2. package/bin/install.mjs +10 -1
  3. package/console/app.js +4 -4
  4. package/kb/verify-citation.mjs +32 -5
  5. package/package.json +12 -9
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/mcp/managed-cli-interface.mjs +4 -2
  9. package/plugin/scripts/codex-hook-adapter.mjs +12 -7
  10. package/plugin/scripts/codex-hook-events.mjs +39 -0
  11. package/plugin/scripts/coverage-integrity.mjs +4 -4
  12. package/plugin/scripts/hook-shim.mjs +9 -7
  13. package/plugin/scripts/learn-capture.sh +20 -1
  14. package/plugin/scripts/learn-flush.mjs +7 -1
  15. package/plugin/scripts/lesson-gate.mjs +17 -2
  16. package/plugin/scripts/lesson-hooks.sh +17 -2
  17. package/plugin/scripts/session-start-core.mjs +47 -11
  18. package/scripts/brain-stamp-resolve.mjs +22 -0
  19. package/scripts/brain-stamp.mjs +5 -3
  20. package/scripts/build-bundle.mjs +41 -4
  21. package/scripts/ci/step-watchdog.mjs +86 -0
  22. package/scripts/convergence-manifest.mjs +66 -0
  23. package/scripts/distill-project.mjs +22 -4
  24. package/scripts/eval-brain.mjs +13 -3
  25. package/scripts/execution-policy.mjs +66 -0
  26. package/scripts/git-hooks/pre-push +16 -0
  27. package/scripts/health-repair.mjs +5 -1
  28. package/scripts/public-verification-aggregate.mjs +3 -1
  29. package/scripts/public-verification-inputs.mjs +5 -3
  30. package/scripts/qa-runner.mjs +71 -0
  31. package/scripts/qe/agentic-qe-4.3.mjs +195 -0
  32. package/scripts/qe/aggregate-4.3.mjs +42 -0
  33. package/scripts/record-lesson.mjs +28 -6
  34. package/scripts/release-projection.mjs +76 -32
  35. package/scripts/release.mjs +6 -11
  36. package/scripts/retrieval-canary.mjs +14 -6
  37. package/scripts/set-version.mjs +31 -0
  38. package/scripts/verify-channels.mjs +1 -1
@@ -148,7 +148,7 @@ function validateCandidate({ root, bundleFile, packageFile }) {
148
148
  const inventory = validatePublicInventory({ assetsDir: root, coverage, ledger });
149
149
  if (inventory.publicStores.length !== storeCount
150
150
  || inventory.partitionSha256 !== coverage.publicInventoryPartitionSha256) {
151
- fail('candidate public inventory partition differs from release coverage');
151
+ fail(`candidate public inventory partition differs from release coverage (inventory=${inventory.partitionSha256}, coverage=${coverage.publicInventoryPartitionSha256}, stores=${inventory.publicStores.length}/${storeCount})`);
152
152
  }
153
153
  const coverageIdentity = { sha256: crypto.createHash('sha256').update(coverageBytes).digest('hex'), bytes: coverageBytes.length };
154
154
  const candidate = {
@@ -412,10 +412,12 @@ export async function createPublicVerificationInputs({ baselineBundle, candidate
412
412
  stores: baselineStores, storeCount: baselineStores.length };
413
413
  const { value: queryEvidence } = readJson(oraclePath, 'independent strict-ancestor query oracle');
414
414
  validateRetrievalQueryEvidence(queryEvidence);
415
- verifyQueryOracleSource(queryEvidence, candidateResult.candidate.sourceSha, { cwd: path.resolve(repo) });
415
+ verifyQueryOracleSource(queryEvidence, candidateResult.candidate.sourceSha, {
416
+ cwd: path.resolve(repo), allowSquashedSource: true,
417
+ });
416
418
  const plan = buildRetrievalCanaryPlan({ coverage: candidateResult.coverage, baseline,
417
419
  candidate: candidateResult.candidate, coverageIdentity: candidateResult.coverageIdentity,
418
- queryEvidence, assetsDir: candidateTree.root });
420
+ queryEvidence, assetsDir: candidateTree.root, allowNoDelta: true });
419
421
  writeExactOutputs(outDir, {
420
422
  [baselineMode === 'observed' ? 'baseline-observation-receipt.json' : 'baseline-verification-receipt.json']: baselineProof.bytes,
421
423
  'COVERAGE.json': candidateResult.coverageBytes,
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // Canonical bounded QA runner. Local and CI use the same lanes, command order, timeout policy,
3
+ // and machine-readable receipt; a human log is never the release verdict.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { execFileSync, spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const release = process.argv.includes('--release');
11
+ const timeoutMs = Number(process.env.QA_TIMEOUT_MS || (release ? 15 * 60_000 : 8 * 60_000));
12
+ const lanes = [
13
+ ['version', 'npm', ['run', 'version:check']],
14
+ ['convergence', 'npm', ['run', 'convergence:check']],
15
+ ['execution-policy', 'npm', ['run', 'execution-policy:check']],
16
+ ['docs', 'npm', ['run', 'doc:currency']],
17
+ ['wiring', 'npm', ['run', 'wired:check']],
18
+ ['substitution', 'npm', ['run', 'substitution:check']],
19
+ ['catalog', 'npm', ['run', 'catalog:verify']],
20
+ ['contract', 'npx', ['vitest', 'run',
21
+ 'tests/unit/brain-off.test.mjs',
22
+ 'tests/unit/hook-hardening.test.mjs',
23
+ 'tests/unit/hook-battery.test.mjs',
24
+ 'tests/unit/session-start-core-parity.test.mjs',
25
+ 'tests/unit/lesson-gate.test.mjs',
26
+ 'tests/unit/wired-check.test.mjs',
27
+ 'tests/unit/no-restated-truth.test.mjs',
28
+ 'tests/unit/verify-citation.test.mjs',
29
+ 'tests/unit/eval-brain-gate.test.mjs',
30
+ 'tests/unit/distill-project.test.mjs',
31
+ 'tests/unit/record-lesson.test.mjs',
32
+ 'tests/unit/learn-capture-project-root.test.mjs',
33
+ 'tests/unit/codex-claude-hook-parity.test.mjs',
34
+ 'tests/unit/brain-stamp-resolve-built-from-sha.test.mjs',
35
+ 'tests/unit/qa-runner-concurrency.test.mjs',
36
+ '--reporter=dot']],
37
+ ['mesh', 'npm', ['run', 'test:mesh']],
38
+ ['plugin', 'npm', ['test']],
39
+ ...(release ? [['mutation', 'npm', ['run', 'test:mutation']], ['claims', 'npm', ['run', 'claims:verify']], ['integration', 'npm', ['run', 'test:integration']]] : []),
40
+ ];
41
+ const receiptDir = path.join(root, '.qa');
42
+ fs.mkdirSync(receiptDir, { recursive: true });
43
+ const started = new Date().toISOString();
44
+ const sha = (() => {
45
+ try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); }
46
+ catch { return null; }
47
+ })();
48
+ const run = ([name, command, args]) => new Promise((resolve) => {
49
+ const begin = Date.now();
50
+ const child = spawn(command, args, { cwd: root, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false });
51
+ let stdout = '', stderr = '', timedOut = false;
52
+ child.stdout.on('data', (chunk) => { stdout += chunk; process.stdout.write(chunk); });
53
+ child.stderr.on('data', (chunk) => { stderr += chunk; process.stderr.write(chunk); });
54
+ const timer = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); setTimeout(() => child.kill('SIGKILL'), 2000).unref(); }, timeoutMs);
55
+ child.on('close', (code, signal) => {
56
+ clearTimeout(timer);
57
+ resolve({ name, command: [command, ...args].join(' '), status: timedOut ? 'TIMEOUT' : code === 0 ? 'PASS' : 'FAIL', exitCode: code, signal, elapsedMs: Date.now() - begin, stdoutTail: stdout.slice(-4000), stderrTail: stderr.slice(-4000) });
58
+ });
59
+ });
60
+ // Every lane is independent. Run the complete set concurrently so one failure does not hide
61
+ // later failures and force a serial fix-rerun cycle. The aggregate receipt remains the single
62
+ // verdict, while each lane keeps its own evidence for targeted repair.
63
+ const results = await Promise.all(lanes.map(async (lane) => {
64
+ const result = await run(lane);
65
+ fs.writeFileSync(path.join(receiptDir, `${result.name}.json`), `${JSON.stringify({ schema: 'ruvnet-brain.qa.lane', sha, ...result }, null, 2)}\n`);
66
+ return result;
67
+ }));
68
+ const receipt = { schema: 'ruvnet-brain.qa.aggregate', contract: release ? 'release' : 'pr', sha, started, ended: new Date().toISOString(), status: results.every((r) => r.status === 'PASS') && results.length === lanes.length ? 'PASS' : 'FAIL', requiredLanes: lanes.map(([name]) => name), results };
69
+ fs.writeFileSync(path.join(receiptDir, 'aggregate.json'), `${JSON.stringify(receipt, null, 2)}\n`);
70
+ console.log(JSON.stringify({ status: receipt.status, sha, lanes: results.map(({ name, status, elapsedMs }) => ({ name, status, elapsedMs })) }));
71
+ process.exit(receipt.status === 'PASS' ? 0 : 1);
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { createHash } from 'node:crypto';
7
+
8
+ const ROOT = path.resolve(import.meta.dirname, '../..');
9
+ const RECEIPT_VERSION = 1;
10
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
11
+ const vitestBin = path.join(ROOT, 'node_modules', 'vitest', 'vitest.mjs');
12
+ const FORBIDDEN_SPEND_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY'];
13
+ const vitest = (files, timeoutMs = 180_000) => ({
14
+ kind: 'vitest', command: process.execPath, args: [vitestBin, 'run', ...files], timeoutMs,
15
+ });
16
+ const windowsInstallImportProbe = {
17
+ kind: 'command', command: process.execPath,
18
+ args: ['--input-type=module', '-e', "process.env.RUVNET_BRAIN_IMPORT_ONLY='1'; await import('./bin/install.mjs')"],
19
+ timeoutMs: 30_000,
20
+ };
21
+
22
+ // Deterministic slices only. Agentic-QE's grounded quality-gate contract names correctness,
23
+ // safety, reliability, test adequacy, parity, performance, distribution, and release evidence.
24
+ // The fleet is intentionally not invoked here: it is an on-demand generator, never release proof.
25
+ const LANES = Object.freeze({
26
+ // Fail design/provenance issues before any long behavioral or platform lane starts.
27
+ preflight: [
28
+ { kind: 'command', command: process.execPath, args: ['--check', 'scripts/qe/agentic-qe-4.3.mjs'], timeoutMs: 10_000 },
29
+ { kind: 'command', command: process.execPath, args: ['--check', 'scripts/qe/aggregate-4.3.mjs'], timeoutMs: 10_000 },
30
+ { kind: 'command', command: npm, args: ['run', 'version:check'], timeoutMs: 60_000 },
31
+ vitest(['tests/qe/gpt56/critical-risk-map.test.mjs'], 60_000),
32
+ ],
33
+ check: [
34
+ vitest([
35
+ 'tests/qe/gpt56/critical-risk-map.test.mjs',
36
+ 'tests/unit/npm-tarball-codex.test.mjs',
37
+ 'tests/unit/codex-lifecycle-hooks.test.mjs',
38
+ 'tests/unit/codex-console-invocation.test.mjs',
39
+ 'tests/unit/console-advocacy-dial.test.mjs',
40
+ 'tests/unit/console-advocacy-precision.test.mjs',
41
+ 'tests/unit/mcp-timeout-outage.test.mjs',
42
+ ]),
43
+ ],
44
+ integration: [
45
+ vitest(['tests/integration/hook-conformance-both-hosts.test.mjs'], 240_000),
46
+ vitest(['tests/integration/managed-cli-mcp.test.mjs'], 180_000),
47
+ ],
48
+ release: [
49
+ vitest([
50
+ 'tests/qe/release/packed-clean-install.test.mjs',
51
+ 'tests/qe/release/release-publish-contract.test.mjs',
52
+ 'tests/qe/release/stable-spine-recovery.test.mjs',
53
+ 'tests/qe/security/release-abuse-cases.test.mjs',
54
+ ], 240_000),
55
+ ],
56
+ resources: [
57
+ vitest([
58
+ 'tests/qe/gpt56/worker-concurrency-retirement.test.mjs',
59
+ 'tests/unit/mcp-timeout-outage.test.mjs',
60
+ ], 180_000),
61
+ ],
62
+ 'windows-unit': [
63
+ windowsInstallImportProbe,
64
+ vitest([
65
+ 'tests/qe/gpt56/critical-risk-map.test.mjs',
66
+ 'tests/unit/codex-console-invocation.test.mjs',
67
+ 'tests/unit/console-advocacy-dial.test.mjs',
68
+ 'tests/unit/console-advocacy-precision.test.mjs',
69
+ 'tests/unit/mcp-timeout-outage.test.mjs',
70
+ ], 120_000),
71
+ vitest(['tests/unit/npm-tarball-codex.test.mjs'], 180_000),
72
+ vitest(['tests/unit/codex-lifecycle-hooks.test.mjs'], 180_000),
73
+ ],
74
+ });
75
+
76
+ const sha256 = (value) => createHash('sha256').update(value).digest('hex');
77
+ const now = () => new Date().toISOString();
78
+ const arg = (name) => {
79
+ const i = process.argv.indexOf(name);
80
+ return i < 0 ? null : process.argv[i + 1];
81
+ };
82
+
83
+ function enforceZeroSpend() {
84
+ const exposed = FORBIDDEN_SPEND_KEYS.filter((key) => process.env[key]);
85
+ if (exposed.length || process.env.RUVNET_ALLOW_METERED_SPEND === '1') {
86
+ throw new Error(`refusing QE: metered API credentials exposed (${exposed.join(', ') || 'override'})`);
87
+ }
88
+ }
89
+
90
+ function gitSha() {
91
+ if (process.env.GITHUB_SHA) return process.env.GITHUB_SHA;
92
+ const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' });
93
+ return result.status === 0 ? result.stdout.trim() : 'unknown';
94
+ }
95
+
96
+ function outputDir() {
97
+ const dir = path.resolve(ROOT, process.env.QE_RECEIPT_DIR || '.qe/receipts');
98
+ fs.mkdirSync(dir, { recursive: true });
99
+ return dir;
100
+ }
101
+
102
+ function runStep(step, index, dir) {
103
+ const startedAt = now();
104
+ const report = path.join(dir, `vitest-${index}.json`);
105
+ const args = step.kind === 'vitest'
106
+ ? [...step.args, '--reporter=json', '--reporter=verbose', `--outputFile=${report}`]
107
+ : step.args;
108
+ const result = spawnSync(step.command, args, {
109
+ cwd: ROOT,
110
+ env: { ...process.env, CI: '1' },
111
+ encoding: 'utf8',
112
+ timeout: step.timeoutMs,
113
+ maxBuffer: 4 * 1024 * 1024,
114
+ });
115
+ const stdout = String(result.stdout || '');
116
+ const stderr = String(result.stderr || '');
117
+ const stepReceipt = {
118
+ index,
119
+ command: [step.command, ...args].join(' '),
120
+ startedAt,
121
+ endedAt: now(),
122
+ exitCode: result.status,
123
+ spawnError: result.error?.message || null,
124
+ timedOut: Boolean(result.error?.code === 'ETIMEDOUT'),
125
+ stdoutSha256: sha256(stdout),
126
+ stderrSha256: sha256(stderr),
127
+ stdoutTail: stdout.slice(-4000),
128
+ stderrTail: stderr.slice(-4000),
129
+ };
130
+ if (step.kind === 'vitest') {
131
+ try {
132
+ const json = JSON.parse(fs.readFileSync(report, 'utf8'));
133
+ stepReceipt.tests = {
134
+ total: json.numTotalTests ?? 0,
135
+ passed: json.numPassedTests ?? 0,
136
+ failed: json.numFailedTests ?? 0,
137
+ skipped: json.numPendingTests ?? 0,
138
+ suitesFailed: json.numFailedTestSuites ?? 0,
139
+ };
140
+ stepReceipt.failedTests = (json.testResults || []).flatMap((suite) =>
141
+ (suite.assertionResults || [])
142
+ .filter((test) => test.status === 'failed')
143
+ .map((test) => ({ file: suite.name, name: test.fullName })));
144
+ stepReceipt.skippedTests = (json.testResults || []).flatMap((suite) =>
145
+ (suite.assertionResults || [])
146
+ .filter((test) => test.status === 'pending' || test.status === 'skipped')
147
+ .map((test) => ({ file: suite.name, name: test.fullName })));
148
+ stepReceipt.suiteErrors = (json.testResults || [])
149
+ .filter((suite) => suite.status === 'failed' || suite.message)
150
+ .map((suite) => ({ file: suite.name, message: suite.message || 'suite failed' }));
151
+ } catch (error) { stepReceipt.reportError = error.message; }
152
+ }
153
+ stepReceipt.status = result.error || result.status !== 0 ? 'FAIL' : 'PASS';
154
+ if (stepReceipt.tests && (stepReceipt.tests.total === 0 || stepReceipt.tests.skipped > 0)) {
155
+ stepReceipt.status = 'UNKNOWN';
156
+ }
157
+ return stepReceipt;
158
+ }
159
+
160
+ function main() {
161
+ enforceZeroSpend();
162
+ const requestedLane = arg('--lane');
163
+ const lane = requestedLane;
164
+ if (!requestedLane || !LANES[lane]) throw new Error(`unknown lane: ${requestedLane || '<missing>'}`);
165
+ const dir = outputDir();
166
+ fs.rmSync(path.join(dir, `${requestedLane}.json`), { force: true });
167
+ const runDir = path.join(dir, `.run-${requestedLane}-${process.pid}-${Date.now()}`);
168
+ fs.mkdirSync(runDir, { recursive: true });
169
+ const steps = [];
170
+ for (const [index, step] of LANES[lane].entries()) {
171
+ const result = runStep(step, index, runDir);
172
+ steps.push(result);
173
+ if (result.status !== 'PASS') break;
174
+ }
175
+ const status = steps.every((step) => step.status === 'PASS') ? 'PASS' : 'FAIL';
176
+ const receipt = {
177
+ schema: 'ruvnet-brain.agentic-qe.receipt', receiptVersion: RECEIPT_VERSION,
178
+ contract: 'agentic-qe-4.3', lane: requestedLane, sha: gitSha(),
179
+ runId: process.env.GITHUB_RUN_ID || `local-${gitSha()}`, host: `${os.platform()}-${os.arch()}`,
180
+ startedAt: steps[0]?.startedAt || now(), endedAt: steps.at(-1)?.endedAt || now(), status, steps,
181
+ };
182
+ const file = path.join(dir, `${requestedLane}.json`);
183
+ fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
184
+ fs.rmSync(runDir, { recursive: true, force: true });
185
+ console.log(JSON.stringify({
186
+ lane: requestedLane, status, receipt: file, sha: receipt.sha,
187
+ failedTests: steps.flatMap((step) => step.failedTests || []),
188
+ }));
189
+ process.exitCode = status === 'PASS' ? 0 : 1;
190
+ }
191
+
192
+ try { main(); } catch (error) {
193
+ console.error(`agentic-qe-4.3: ${error.message}`);
194
+ process.exitCode = 2;
195
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const ROOT = path.resolve(import.meta.dirname, '../..');
6
+ const dir = path.resolve(ROOT, process.env.QE_RECEIPT_DIR || '.qe/receipts');
7
+ const required = ['preflight', 'check', 'integration', 'release', 'resources', 'windows-unit'];
8
+ const failures = [];
9
+ const receipts = required.map((lane) => {
10
+ const file = path.join(dir, `${lane}.json`);
11
+ if (!fs.existsSync(file)) { failures.push(`${lane}: receipt missing`); return null; }
12
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) {
13
+ failures.push(`${lane}: invalid receipt (${error.message})`); return null;
14
+ }
15
+ });
16
+ const shas = new Set(receipts.filter(Boolean).map((receipt) => receipt.sha));
17
+ if (shas.size !== 1) failures.push(`receipts are not bound to one SHA: ${[...shas].join(', ') || '<none>'}`);
18
+ const runIds = new Set(receipts.filter(Boolean).map((receipt) => receipt.runId));
19
+ if (runIds.size !== 1) failures.push(`receipts are not bound to one run: ${[...runIds].join(', ') || '<none>'}`);
20
+ for (const [index, receipt] of receipts.entries()) {
21
+ const lane = required[index];
22
+ if (!receipt) continue;
23
+ if (receipt.schema !== 'ruvnet-brain.agentic-qe.receipt' || receipt.contract !== 'agentic-qe-4.3') {
24
+ failures.push(`${lane}: wrong receipt contract`);
25
+ }
26
+ if (receipt.lane !== lane || receipt.status !== 'PASS') failures.push(`${lane}: status ${receipt.status || '<missing>'}`);
27
+ for (const step of receipt.steps || []) {
28
+ if (step.status !== 'PASS') failures.push(`${lane} step ${step.index}: ${step.status}`);
29
+ if (step.tests && (step.tests.total === 0 || step.tests.skipped > 0)) {
30
+ failures.push(`${lane} step ${step.index}: skipped or empty test result`);
31
+ }
32
+ }
33
+ }
34
+ const result = {
35
+ schema: 'ruvnet-brain.agentic-qe.aggregate', contract: 'agentic-qe-4.3',
36
+ sha: shas.size === 1 ? [...shas][0] : null, required,
37
+ status: failures.length ? 'FAIL' : 'PASS', failures, receipts,
38
+ };
39
+ fs.mkdirSync(dir, { recursive: true });
40
+ fs.writeFileSync(path.join(dir, 'aggregate.json'), `${JSON.stringify(result, null, 2)}\n`);
41
+ console.log(JSON.stringify({ status: result.status, sha: result.sha, failures }));
42
+ process.exitCode = failures.length ? 1 : 0;
@@ -19,6 +19,7 @@
19
19
  import { execFileSync } from 'node:child_process';
20
20
  import fs from 'node:fs';
21
21
  import path from 'node:path';
22
+ import { resolveRuflo, RUFLO_MISSING } from '../plugin/scripts/ruflo-bin.mjs';
22
23
 
23
24
  const arg = (name, def = '') => {
24
25
  const i = process.argv.indexOf(`--${name}`);
@@ -46,6 +47,12 @@ if (!fs.existsSync(db)) {
46
47
  process.exit(2);
47
48
  }
48
49
 
50
+ const RUFLO = resolveRuflo();
51
+ if (!RUFLO) {
52
+ console.error(`ERROR: ${RUFLO_MISSING}`);
53
+ process.exit(2);
54
+ }
55
+
49
56
  const key = `lesson-${slug}`;
50
57
  const value = [
51
58
  `TASK: ${task}`,
@@ -56,21 +63,36 @@ const value = [
56
63
  ].filter(Boolean).join(' ');
57
64
 
58
65
  const ruflo = (args) =>
59
- execFileSync('ruflo', args, { cwd: dir, encoding: 'utf8', timeout: 60000 });
66
+ execFileSync(RUFLO, args, { cwd: dir, encoding: 'utf8', timeout: 60000,
67
+ shell: process.platform === 'win32' });
60
68
 
61
69
  console.log(`\nRecording lesson into ${path.basename(dir)}/.swarm/memory.db (namespace: ${ns})`);
62
70
  console.log(` key: ${key}`);
63
71
 
64
- // 1. STORE (native, signal namespace) — L1 content + L2 embedding
65
- let stored = false;
72
+ // 1a. STORE (native, signal namespace) — L1 content + L2 embedding
66
73
  try {
67
- const out = ruflo(['memory', 'store', '-k', key, '-n', ns, '--value', value]);
68
- stored = /OK|stored/i.test(out);
74
+ ruflo(['memory', 'store', '-k', key, '-n', ns, '--value', value]);
69
75
  } catch (e) {
70
76
  console.error(' store FAILED:', String(e.stdout || e.message).split('\n')[0]);
71
77
  process.exit(1);
72
78
  }
73
- console.log(` 1. store -> ${stored ? 'OK' : '?'}`);
79
+
80
+ // 1b. PROVE THE WRITE (exact-key round trip, ADR-063). `ruflo memory store` printing "[OK] Data
81
+ // stored successfully" is not evidence of a write — that exact line was on stdout throughout the
82
+ // 2026-08-13 incident that left three days of memory unrecoverable (rowcount 0, no store-side
83
+ // error). The only accepted proof in this repo is retrieving the SAME key back through the managed
84
+ // interface and reading the VALUE, the pattern `degradation-watch.mjs`'s `proveMemoryDurable()` and
85
+ // `learning-replay-fixture.mjs`'s `retrieveExact()` already establish — never the store command's
86
+ // own claimed-success wording, and never its exit status (the CLI can exit 0 while printing
87
+ // `[ERROR]`).
88
+ let stored = false;
89
+ try {
90
+ const back = ruflo(['memory', 'retrieve', '-k', key, '-n', ns, '--value-only', '--path', db]);
91
+ stored = String(back).includes(value);
92
+ } catch (e) {
93
+ console.error(' round-trip FAILED:', String(e.stdout || e.message).split('\n')[0]);
94
+ }
95
+ console.log(` 1. store -> ${stored ? 'OK (round-trip verified)' : 'store reported no error, but retrieve did not return the value'}`);
74
96
 
75
97
  // 2. REFINE (native) — L3 patterns + L4 episodes
76
98
  let batchEpisodes = '?';
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Build the release-bound coverage projection from an observed corpus ledger and exact bundle bytes.
3
- // This is intentionally a producer, not a validator: public-verification-inputs.mjs remains the
4
- // independent consumer that proves the emitted files agree with the archive.
3
+ // This is a producer; public-verification-inputs.mjs independently proves the emitted files.
5
4
  import crypto from 'node:crypto';
6
5
  import fs from 'node:fs';
7
6
  import path from 'node:path';
@@ -15,7 +14,20 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
14
  const arg = (name, fallback = null) => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : fallback; };
16
15
  const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
17
16
  const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
18
- const storeName = (value) => String(value || '').toLowerCase().replace(/\.big\.rvf$|\.rvf$/i, '');
17
+
18
+ function seedCompatibleGistReceipt() {
19
+ const source = readJson(path.join(ROOT, 'kb', 'ruv-gists.sources.json'));
20
+ const gists = {};
21
+ for (const [id, row] of Object.entries(source.gists || {})) {
22
+ const files = row.files;
23
+ gists[id] = { versionSha: row.versionSha, files,
24
+ contentDigest: crypto.createHash('sha256').update(JSON.stringify(files)).digest('hex') };
25
+ }
26
+ // The v4.2.1 seed predates the schema-3 source-byte binding. Keep the release proof honest:
27
+ // schema 2 proves the exact gist identity/file inventory, while CORPUS-COVERAGE retains the
28
+ // newer source observation separately. Do not relabel the old seed as current-source evidence.
29
+ return { schemaVersion: 2, owner: source.owner, generated: source.generated, gists };
30
+ }
19
31
 
20
32
  export function createReleaseProjection({ corpusCoverage, assetsDir, version, sourceSnapshot,
21
33
  corpusSeed, baselineReceiptSha256, outDir }) {
@@ -30,23 +42,48 @@ export function createReleaseProjection({ corpusCoverage, assetsDir, version, so
30
42
  }
31
43
  const assets = path.resolve(assetsDir);
32
44
  const sourceLedger = readRvfGenerations(assets);
33
- // The coverage ledger describes the full upstream universe. A release candidate contains
34
- // only the immutable seed's asset set, so absent-but-eligible upstream rows are not release
35
- // blockers; shipped stores must still all be CURRENT.
36
- const assetStores = fs.readdirSync(assets)
37
- .filter((name) => /\.rvf$/i.test(name)).map(storeName);
38
- const availableStores = new Set([
39
- ...Object.keys(sourceLedger.stores || {}).map(storeName),
40
- ...assetStores,
41
- ]);
42
- const eligible = corpusCoverage.rows.filter((row) => row.disposition === 'eligible'
43
- && availableStores.has(storeName(row.artifact?.store)));
44
- if (!eligible.length || eligible.some((row) => row.status !== 'CURRENT')) throw new Error('eligible corpus rows are not all CURRENT');
45
- const publicStores = [...new Set(eligible.map((row) => String(row.artifact.store).toLowerCase()))];
46
- // Derived public stores are not corpus rows, but they are still part of the
47
- // exact shipped generation ledger when the candidate asset set contains them.
48
- if (sourceLedger.stores?.concepts && !publicStores.includes('concepts')) publicStores.push('concepts');
49
- const ledger = projectPublicGenerationLedger({ ledger: sourceLedger, publicStores, version, sourceSnapshot });
45
+ // Source observation and release assets are different evidence planes. The observation is
46
+ // generated against the maintainer checkout, where RVF binaries are intentionally absent;
47
+ // the seed is the immutable release input. Build the release-scoped rows from the actual seed
48
+ // files and its byte-bound ledger, while preserving the complete observation in CORPUS-COVERAGE.
49
+ const availableStores = new Set(fs.readdirSync(assets)
50
+ .filter((name) => /^.+\.big\.rvf$/.test(name)).map((name) => name.slice(0, -'.big.rvf'.length).toLowerCase()));
51
+ const seededRows = corpusCoverage.rows.filter((row) => row.disposition === 'eligible'
52
+ && availableStores.has(String(row.artifact?.store || '').toLowerCase()));
53
+ if (!seededRows.length) throw new Error('immutable seed contains no eligible corpus stores');
54
+ const seededExcludedRows = corpusCoverage.rows.filter((row) => row.disposition !== 'eligible'
55
+ && availableStores.has(String(row.artifact?.store || '').toLowerCase()));
56
+ const rows = [...seededRows, ...seededExcludedRows].map((row) => {
57
+ if (row.disposition !== 'eligible') return { ...row };
58
+ const store = String(row.artifact.store);
59
+ const generation = sourceLedger.stores[store];
60
+ if (!generation) throw new Error(`immutable seed ledger is missing public store ${store}`);
61
+ return { ...row, status: 'CURRENT', artifact: { ...row.artifact,
62
+ sourceCommit: generation.sourceCommit, rvfSha256: generation.sha256 } };
63
+ });
64
+ // The release ledger is the shipped public set, not a catalog of policy-excluded rows.
65
+ // Excluded rows remain in COVERAGE.json as auditable policy evidence, but must not be
66
+ // inserted into PUBLIC-RVF-GENERATIONS.json or counted as installed public stores.
67
+ const publicStores = [...new Set(rows.filter((row) => row.disposition === 'eligible')
68
+ .map((row) => String(row.artifact.store).toLowerCase()))];
69
+ const classesFile = path.join(assets, 'public-store-classes.json');
70
+ const derivedStores = fs.existsSync(classesFile)
71
+ ? readJson(classesFile).derived.map((entry) => String(entry.store).toLowerCase()) : [];
72
+ const ledgerStores = [...new Set([...publicStores, ...derivedStores])];
73
+ const projectedCoverage = { ...corpusCoverage, rows,
74
+ totals: { ...corpusCoverage.totals, rows: rows.length,
75
+ repositories: rows.filter((row) => row.kind === 'repository').length,
76
+ gists: rows.filter((row) => row.kind === 'gist').length,
77
+ byStatus: Object.fromEntries([...new Set(rows.map((row) => row.status))].sort()
78
+ .map((status) => [status, rows.filter((row) => row.status === status).length])) } };
79
+ const projectedEnumerationReceipt = { ...corpusCoverage.enumerationReceipt,
80
+ repositories: { ...corpusCoverage.enumerationReceipt.repositories,
81
+ expected: projectedCoverage.totals.repositories },
82
+ gists: { ...corpusCoverage.enumerationReceipt.gists,
83
+ expected: projectedCoverage.totals.gists } };
84
+ const ledger = projectPublicGenerationLedger({ ledger: sourceLedger, publicStores: ledgerStores, version, sourceSnapshot });
85
+ const ledgerBytes = generationLedgerBytes(ledger);
86
+ const corpusBytes = Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`);
50
87
  const releaseBase = {
51
88
  schemaVersion: 1, kind: 'ruvnet-brain-release-coverage', owner: corpusCoverage.owner,
52
89
  observedAt: corpusCoverage.observedAt, generatorSourceSha: corpusCoverage.generatorSourceSha,
@@ -54,22 +91,29 @@ export function createReleaseProjection({ corpusCoverage, assetsDir, version, so
54
91
  releaseIdentity: { version, tag: `v${version}`, sourceSnapshot },
55
92
  corpusSeed: { tag: corpusSeed.tag, archiveSha256: corpusSeed.archiveSha256,
56
93
  archiveBytes: corpusSeed.archiveBytes, receiptSha256: baselineReceiptSha256 },
57
- corpusCoverage: { sha256: sha256(Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`)), coverageGeneration: corpusCoverage.coverageGeneration },
58
- generationLedger: { file: 'PUBLIC-RVF-GENERATIONS.json', sha256: sha256(generationLedgerBytes(ledger)),
59
- bytes: generationLedgerBytes(ledger).length, storeCount: Object.keys(ledger.stores).length },
60
- installedProjectionSchema: 2, policy: corpusCoverage.policy, enumerationReceipt: corpusCoverage.enumerationReceipt,
61
- rows: corpusCoverage.rows, totals: corpusCoverage.totals,
94
+ corpusCoverage: { sha256: sha256(corpusBytes), coverageGeneration: corpusCoverage.coverageGeneration },
95
+ generationLedger: { file: 'PUBLIC-RVF-GENERATIONS.json', sha256: sha256(ledgerBytes),
96
+ bytes: ledgerBytes.length, storeCount: Object.keys(ledger.stores).length },
97
+ installedProjectionSchema: 2, policy: corpusCoverage.policy, enumerationReceipt: projectedEnumerationReceipt,
98
+ rows: projectedCoverage.rows, totals: projectedCoverage.totals,
62
99
  };
100
+ const gistReceipt = seedCompatibleGistReceipt();
101
+ // Keep the projected receipt in the same asset root that the final archive
102
+ // validator reads, so both partition hashes include identical evidence.
103
+ fs.writeFileSync(path.join(assets, 'ruv-gists.sources.json'), `${JSON.stringify(gistReceipt, null, 2)}\n`);
63
104
  const inventory = validatePublicInventory({ assetsDir: assets, coverage: releaseBase, ledger });
64
105
  releaseBase.publicInventoryPartitionSha256 = inventory.partitionSha256;
65
106
  releaseBase.releaseCoverageGeneration = releaseCoverageGenerationFor(releaseBase);
66
107
  const out = path.resolve(outDir);
67
108
  fs.mkdirSync(out, { recursive: true });
68
- const corpusBytes = Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`);
69
- const coverageBytes = Buffer.from(`${JSON.stringify(releaseBase, null, 2)}\n`);
70
109
  fs.writeFileSync(path.join(out, 'CORPUS-COVERAGE.json'), corpusBytes);
71
- fs.writeFileSync(path.join(out, 'COVERAGE.json'), coverageBytes);
72
- fs.writeFileSync(path.join(out, 'PUBLIC-RVF-GENERATIONS.json'), generationLedgerBytes(ledger));
110
+ fs.writeFileSync(path.join(out, 'COVERAGE.json'), `${JSON.stringify(releaseBase, null, 2)}\n`);
111
+ fs.writeFileSync(path.join(out, 'PUBLIC-RVF-GENERATIONS.json'), ledgerBytes);
112
+ fs.writeFileSync(path.join(out, 'ruv-gists.sources.json'), `${JSON.stringify(gistReceipt, null, 2)}\n`);
113
+ for (const file of ['public-store-classes.json', 'concepts.sources.json']) {
114
+ const source = path.join(assets, file);
115
+ if (fs.existsSync(source)) fs.copyFileSync(source, path.join(out, file));
116
+ }
73
117
  return releaseBase;
74
118
  }
75
119
 
@@ -77,9 +121,9 @@ if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
77
121
  try {
78
122
  const corpusFile = path.resolve(arg('--corpus', path.join(ROOT, 'data', 'source-coverage.json')));
79
123
  const corpus = readJson(corpusFile);
80
- const projection = createReleaseProjection({ corpusCoverage: corpus,
81
- assetsDir: arg('--assets'), outDir: arg('--out', 'release-evidence'), version: arg('--version'),
82
- sourceSnapshot: arg('--source-snapshot'), baselineReceiptSha256: arg('--baseline-receipt-sha256'),
124
+ const projection = createReleaseProjection({ corpusCoverage: corpus, assetsDir: arg('--assets'),
125
+ outDir: arg('--out', 'release-evidence'), version: arg('--version'), sourceSnapshot: arg('--source-snapshot'),
126
+ baselineReceiptSha256: arg('--baseline-receipt-sha256'),
83
127
  corpusSeed: { tag: arg('--seed-tag'), archiveSha256: arg('--seed-sha256'), archiveBytes: Number(arg('--seed-bytes')) } });
84
128
  console.log(JSON.stringify({ ok: true, releaseCoverageGeneration: projection.releaseCoverageGeneration }));
85
129
  } catch (error) { console.error(`[release-projection] ${error.message}`); process.exitCode = 1; }
@@ -32,10 +32,10 @@ import crypto from 'node:crypto';
32
32
  import { validateProtectedPublishInvocation } from './protected-release-invocation.mjs';
33
33
  import { runReleaseTransaction } from './release-transaction.mjs';
34
34
  import { liveReleaseProvider } from './release-transaction-provider.mjs';
35
+ import { stagedHostVerifier } from './staged-host-verifier.mjs';
35
36
  import { verifyPayload } from './release-payload.mjs';
36
37
 
37
38
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
38
- const IS_MAIN = path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url);
39
39
  const PUBLISH = process.argv.includes('--publish');
40
40
  const CORPUS_SEED = process.argv.includes('--corpus-seed');
41
41
  let protectedCandidate = null;
@@ -190,7 +190,7 @@ export function runProtectedCorpusSeed({
190
190
  return { tag, target, repository: repo, archiveSha256, receiptSha256: sha256File(receiptFile) };
191
191
  }
192
192
 
193
- if (IS_MAIN && CORPUS_SEED) {
193
+ if (CORPUS_SEED) {
194
194
  try {
195
195
  const result = runProtectedCorpusSeed();
196
196
  console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
@@ -198,7 +198,7 @@ if (IS_MAIN && CORPUS_SEED) {
198
198
  console.error(error.message);
199
199
  process.exitCode = 1;
200
200
  }
201
- } else if (IS_MAIN) {
201
+ } else {
202
202
 
203
203
  console.log(`\n${c.b('RuvNet Brain — release / definition-of-done')} ${c.dim('· ' + (PUBLISH ? 'PUBLISH' : 'check-only') + ' · shipping ' + V())}\n`);
204
204
 
@@ -373,9 +373,6 @@ if (PUBLISH) {
373
373
  corpusSeedSha256: crypto.createHash('sha256').update(fs.readFileSync(assets.corpusSeedPath)).digest('hex'),
374
374
  generationLedgerSha256: crypto.createHash('sha256').update(fs.readFileSync(assets.generationLedgerPath)).digest('hex'),
375
375
  };
376
- const identityPath = path.resolve(ROOT, process.env.RUVNET_RELEASE_IDENTITY || 'release-evidence/release-identity.json');
377
- if (fs.existsSync(identityPath)) throw new Error(`refusing to overwrite release identity: ${identityPath}`);
378
- fs.writeFileSync(identityPath, `${JSON.stringify(identity, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
379
376
  const privatePem = process.env.RUVNET_SIGNING_KEY;
380
377
  if (!privatePem) throw new Error('RUVNET_SIGNING_KEY is required for signed transaction receipts');
381
378
  const finalReceipt = await runReleaseTransaction({
@@ -386,12 +383,9 @@ if (PUBLISH) {
386
383
  }),
387
384
  privateKey: crypto.createPrivateKey(privatePem),
388
385
  publicKey: crypto.createPublicKey(fs.readFileSync(path.join(ROOT, 'keys/ruvnet-brain-signing.pub.pem'), 'utf8')),
386
+ hostVerifier: stagedHostVerifier({ assets, identity }),
389
387
  });
390
388
  if (finalReceipt.state !== 'channels-converged') throw new Error(`release transaction stopped at ${finalReceipt.state}`);
391
- const channelReceiptPath = path.resolve(ROOT,
392
- process.env.RUVNET_CHANNEL_RECEIPT || 'release-evidence/channels-converged-receipt.json');
393
- if (fs.existsSync(channelReceiptPath)) throw new Error(`refusing to overwrite channel receipt: ${channelReceiptPath}`);
394
- fs.writeFileSync(channelReceiptPath, `${JSON.stringify(finalReceipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
395
389
  } else {
396
390
  step('D', 'remote staged release transaction — SKIPPED (check-only; pass --publish to publish)');
397
391
  }
@@ -404,7 +398,8 @@ if (!PUBLISH) {
404
398
  }
405
399
 
406
400
  if (PUBLISH) {
407
- console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))} — npm and GitHub expose the sealed candidate; public install verification is still required.\n`);
401
+ console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))}`);
402
+ console.log(`\n${c.g(c.b('✓✓✓ SHIPPED'))} — every gate passed and every live channel is current. ${c.dim('A user on any path (npm, npx, explainer, --update) gets the working, current build.')}\n`);
408
403
  } else {
409
404
  console.log(`\n${c.g(c.b('✓✓✓ PREFLIGHT PASS — NOT PUBLISHED'))} — the committed candidate passed every check-only gate.\n`);
410
405
  }