ruvnet-brain 4.3.9 → 4.3.11
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 +26 -9
- package/bin/install.mjs +646 -366
- package/bin/nightly-refresh.mjs +115 -0
- package/kb/brain-profile.mjs +118 -32
- package/kb/lifecycle-evidence-retention.mjs +237 -0
- package/kb/refresh-run.mjs +367 -0
- package/kb/retrieval-result.mjs +39 -0
- package/kb/update-storage-transaction.mjs +439 -0
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +2 -165
- package/plugin/hooks/hook-contracts.json +4 -65
- package/plugin/hooks/hooks.json +2 -208
- package/plugin/scripts/adr-currency-gate.mjs +29 -19
- package/plugin/scripts/capability-registry.mjs +10 -83
- package/plugin/scripts/codex-hook-adapter.mjs +62 -9
- package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
- package/plugin/scripts/continuation-gate.mjs +30 -28
- package/plugin/scripts/continuation-objective.mjs +34 -0
- package/plugin/scripts/development-maintenance.mjs +49 -0
- package/plugin/scripts/hook-shim.mjs +3 -0
- package/plugin/scripts/learn-flush.mjs +6 -1
- package/plugin/scripts/lesson-gate.mjs +5 -2
- package/plugin/scripts/lesson-presentation.mjs +5 -0
- package/plugin/scripts/md-stamp.mjs +109 -6
- package/plugin/scripts/memory-doctor.mjs +3 -7
- package/plugin/scripts/nightly-controller.mjs +9 -27
- package/plugin/scripts/nightly-scheduler.mjs +494 -0
- package/plugin/scripts/project-progression-contract.mjs +4 -4
- package/plugin/scripts/project-progression-store.mjs +35 -6
- package/plugin/scripts/ruflo-bin.mjs +20 -0
- package/plugin/scripts/session-snapshot-contract.mjs +6 -1
- package/plugin/scripts/version-bump-gate.sh +14 -2
- package/plugin/skills/release-proof/SKILL.md +6 -2
- package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
- package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
- package/scripts/build-bundle.mjs +8 -0
- package/scripts/build-primer.mjs +2 -3
- package/scripts/candidate-host-evidence.mjs +59 -41
- package/scripts/ci/build-fixture-kb.mjs +59 -2
- package/scripts/ci/mutate-hook-timeout.mjs +24 -47
- package/scripts/ci/stranger-scenario.mjs +13 -28
- package/scripts/claims-verify.mjs +219 -21
- package/scripts/console-engine.mjs +4 -18
- package/scripts/console-runtime-identity.mjs +6 -0
- package/scripts/development-maintenance.mjs +47 -0
- package/scripts/development-push-check.mjs +35 -0
- package/scripts/distill-project.mjs +24 -47
- package/scripts/doc-currency.mjs +91 -19
- package/scripts/git-hooks/pre-push +4 -162
- package/scripts/health-repair.mjs +22 -4
- package/scripts/hook-retirement-check.mjs +22 -0
- package/scripts/host-install-matrix.mjs +125 -61
- package/scripts/integration-evidence.mjs +11 -6
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/nightly-package-observation.mjs +67 -0
- package/scripts/nightly-two-run-proof.mjs +489 -0
- package/scripts/nightly-watchdog.mjs +17 -2
- package/scripts/npm-invocation.mjs +20 -0
- package/scripts/prepublication-evidence.mjs +42 -6
- package/scripts/primer-grounding.mjs +20 -0
- package/scripts/product-integrity-contract.mjs +22 -2
- package/scripts/public-verification-abandon.mjs +101 -0
- package/scripts/public-verification-aggregate.mjs +61 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +64 -8
- package/scripts/publication-receipt.mjs +156 -50
- package/scripts/published-surface-probe.mjs +10 -2
- package/scripts/qa-contract.mjs +57 -0
- package/scripts/qa-lanes.mjs +40 -0
- package/scripts/qa-runner.mjs +54 -64
- package/scripts/qe/ux-suite.mjs +10 -34
- package/scripts/qualified-candidate-check.mjs +124 -0
- package/scripts/release-abort-stale.mjs +2 -2
- package/scripts/release-projection.mjs +20 -1
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/remedy-registry.mjs +6 -13
- package/scripts/retrieval-canary.mjs +91 -18
- package/scripts/selfcheck.mjs +12 -4
- package/scripts/snapshot-freshness.mjs +58 -0
- package/scripts/stack-sync.mjs +41 -43
- package/scripts/staged-host-verifier.mjs +63 -14
- package/scripts/wired-check.mjs +31 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Shared producers for local diagnostics, CI, and candidate qualification.
|
|
2
|
+
export function selectLanes(lanes, requested) {
|
|
3
|
+
if (!requested.length) return lanes;
|
|
4
|
+
const selected = new Set();
|
|
5
|
+
const visit = (name) => {
|
|
6
|
+
const lane = lanes.find((entry) => entry.name === name);
|
|
7
|
+
if (!lane) throw new Error(`unknown QA lane: ${name}`);
|
|
8
|
+
if (selected.has(name)) return;
|
|
9
|
+
selected.add(name);
|
|
10
|
+
for (const dependency of lane.dependsOn || []) visit(dependency);
|
|
11
|
+
};
|
|
12
|
+
requested.forEach(visit);
|
|
13
|
+
return lanes.filter(({ name }) => selected.has(name));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function qaLanes({ release = false, base = null, runtimeCensusArgs = [] } = {}) {
|
|
17
|
+
const node = (name, script, args = [], extra = {}) => ({ name, command: process.execPath, args: [script, ...args], resource: 'static', ...extra });
|
|
18
|
+
const test = (name, args) => node(name, 'node_modules/vitest/vitest.mjs', ['run', ...args], { resource: 'tests' });
|
|
19
|
+
return [
|
|
20
|
+
node('version', 'scripts/sync-version.mjs', ['--check']),
|
|
21
|
+
node('convergence', 'scripts/convergence-manifest.mjs'),
|
|
22
|
+
node('execution-policy', 'scripts/execution-policy.mjs', ['{"action":"delegate","description":"architecture audit","nativeHosts":["codex"]}']),
|
|
23
|
+
node('architecture', 'scripts/product-integrity-contract.mjs', ['--check-source']),
|
|
24
|
+
node('docs', 'scripts/doc-currency.mjs', ['--check', ...(base ? ['--changed', base] : ['--warn-drift'])]),
|
|
25
|
+
node('wiring', 'scripts/wired-check.mjs', ['--check']),
|
|
26
|
+
node('substitution', 'scripts/no-silent-substitution.mjs'),
|
|
27
|
+
node('catalog', 'scripts/verify-model-catalog.mjs'),
|
|
28
|
+
test('coverage', ['tests/unit', '--coverage']),
|
|
29
|
+
node('claims-source', 'scripts/claims-verify.mjs', ['--strict', '--scope', 'source'], { dependsOn: ['coverage'], report: 'claims' }),
|
|
30
|
+
test('mesh', ['tests/mesh']),
|
|
31
|
+
node('plugin', 'plugin/test/run-tests.mjs', [], { resource: 'tests' }),
|
|
32
|
+
...(release ? [
|
|
33
|
+
test('mutation', ['tests/mutation']),
|
|
34
|
+
test('regression', ['tests/regression']),
|
|
35
|
+
test('integration', ['tests/integration']),
|
|
36
|
+
test('continuity', ['tests/acceptance/cross-host-project-resume.test.mjs']),
|
|
37
|
+
node('claims-runtime', 'scripts/claims-verify.mjs', ['--strict', '--scope', 'runtime', ...runtimeCensusArgs], { report: 'claims' }),
|
|
38
|
+
] : []),
|
|
39
|
+
];
|
|
40
|
+
}
|
package/scripts/qa-runner.mjs
CHANGED
|
@@ -1,72 +1,62 @@
|
|
|
1
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
2
|
import fs from 'node:fs';
|
|
5
3
|
import path from 'node:path';
|
|
6
|
-
import
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { qaLanes, selectLanes } from './qa-lanes.mjs';
|
|
8
|
+
import { runLanes, verdictOf, sourceIdentity } from './qa-contract.mjs';
|
|
8
9
|
|
|
9
10
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
11
|
const release = process.argv.includes('--release');
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
'tests/unit/execution-preflight.test.mjs',
|
|
37
|
-
'--reporter=dot']],
|
|
38
|
-
['mesh', 'npm', ['run', 'test:mesh']],
|
|
39
|
-
['plugin', 'npm', ['test']],
|
|
40
|
-
...(release ? [['mutation', 'npm', ['run', 'test:mutation']], ['claims', 'npm', ['run', 'claims:verify']], ['integration', 'npm', ['run', 'test:integration']]] : []),
|
|
41
|
-
];
|
|
42
|
-
const receiptDir = path.join(root, '.qa');
|
|
43
|
-
fs.mkdirSync(receiptDir, { recursive: true });
|
|
44
|
-
const started = new Date().toISOString();
|
|
45
|
-
const sha = (() => {
|
|
46
|
-
try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); }
|
|
47
|
-
catch { return null; }
|
|
48
|
-
})();
|
|
49
|
-
const run = ([name, command, args]) => new Promise((resolve) => {
|
|
50
|
-
const begin = Date.now();
|
|
51
|
-
const child = spawn(command, args, { cwd: root, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false });
|
|
52
|
-
let stdout = '', stderr = '', timedOut = false;
|
|
53
|
-
child.stdout.on('data', (chunk) => { stdout += chunk; process.stdout.write(chunk); });
|
|
54
|
-
child.stderr.on('data', (chunk) => { stderr += chunk; process.stderr.write(chunk); });
|
|
55
|
-
const timer = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); setTimeout(() => child.kill('SIGKILL'), 2000).unref(); }, timeoutMs);
|
|
56
|
-
child.on('close', (code, signal) => {
|
|
57
|
-
clearTimeout(timer);
|
|
58
|
-
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) });
|
|
59
|
-
});
|
|
12
|
+
const argument = (name) => { const i = process.argv.indexOf(name); return i < 0 ? null : process.argv[i + 1]; };
|
|
13
|
+
const requested = process.argv.flatMap((value, index) => value === '--lane' ? [process.argv[index + 1]] : []);
|
|
14
|
+
const runtimeCensusArgs = ['--candidate-kb', '--candidate-sha', '--candidate-version', '--candidate-root',
|
|
15
|
+
'--payload-manifest', '--payload-signature', '--payload-id', '--qualification-mode'].flatMap((name) => {
|
|
16
|
+
if (!process.argv.includes(name)) return [];
|
|
17
|
+
const value = argument(name);
|
|
18
|
+
if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`);
|
|
19
|
+
return [name, value];
|
|
60
20
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
21
|
+
const lanes = selectLanes(qaLanes({ release, runtimeCensusArgs, base: argument('--base') || process.env.QA_BASE_SHA }), requested);
|
|
22
|
+
if (process.argv.includes('--list')) {
|
|
23
|
+
console.log(JSON.stringify(lanes, null, 2));
|
|
24
|
+
} else {
|
|
25
|
+
const timeoutMs = Number(process.env.QA_TIMEOUT_MS || (release ? 15 * 60_000 : 8 * 60_000));
|
|
26
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error('invalid QA timeout');
|
|
27
|
+
const receiptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-qa-'));
|
|
28
|
+
const started = new Date().toISOString();
|
|
29
|
+
const source = sourceIdentity(root);
|
|
30
|
+
const run = (lane) => new Promise((resolve) => {
|
|
31
|
+
const begin = Date.now();
|
|
32
|
+
const evidenceFile = lane.report ? path.join(receiptDir, lane.name + '-evidence.json') : null;
|
|
33
|
+
const args = evidenceFile ? [...lane.args, '--report', evidenceFile] : lane.args;
|
|
34
|
+
const child = spawn(lane.command, args, { cwd: root, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false, detached: process.platform !== 'win32' });
|
|
35
|
+
let stdout = '', stderr = '', timedOut = false, spawnError = null, escalation = null;
|
|
36
|
+
child.stdout.on('data', (chunk) => { stdout = (stdout + chunk).slice(-4000); process.stdout.write(chunk); });
|
|
37
|
+
child.stderr.on('data', (chunk) => { stderr = (stderr + chunk).slice(-4000); process.stderr.write(chunk); });
|
|
38
|
+
child.on('error', (error) => { spawnError = error.message; });
|
|
39
|
+
const kill = (signal) => {
|
|
40
|
+
if (!child.pid) return;
|
|
41
|
+
try {
|
|
42
|
+
if (process.platform === 'win32') spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore' }).on('error', () => {});
|
|
43
|
+
else process.kill(-child.pid, signal);
|
|
44
|
+
} catch (error) { if (error.code !== 'ESRCH') spawnError = error.message; }
|
|
45
|
+
};
|
|
46
|
+
const timer = setTimeout(() => { timedOut = true; kill('SIGTERM'); escalation = setTimeout(() => kill('SIGKILL'), 2000); escalation.unref(); }, timeoutMs);
|
|
47
|
+
child.on('close', (code, signal) => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
clearTimeout(escalation);
|
|
50
|
+
resolve({ name: lane.name, command: [lane.command, ...args], evidenceFile, status: timedOut ? 'TIMEOUT' : spawnError ? 'FAIL' : code === 0 ? 'PASS' : code === 4 ? 'UNKNOWN' : 'FAIL', exitCode: code, signal, spawnError, elapsedMs: Date.now() - begin, stdoutTail: stdout, stderrTail: stderr });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
const results = await runLanes(lanes, run, 2);
|
|
54
|
+
const after = sourceIdentity(root);
|
|
55
|
+
const stable = after.digest === source.digest;
|
|
56
|
+
const status = stable ? verdictOf(results) : 'UNKNOWN';
|
|
57
|
+
const receipt = { schema: 'ruvnet-brain.qa.aggregate', contract: release ? 'release' : 'pr', selection: requested.length ? 'partial' : 'complete', source, sourceAfter: after, sourceStable: stable, started, ended: new Date().toISOString(), status, requiredLanes: lanes.map(({ name }) => name), results };
|
|
58
|
+
for (const result of results) fs.writeFileSync(path.join(receiptDir, result.name + '.json'), JSON.stringify({ schema: 'ruvnet-brain.qa.lane', source, ...result }, null, 2), { flag: 'wx' });
|
|
59
|
+
fs.writeFileSync(path.join(receiptDir, 'aggregate.json'), JSON.stringify(receipt, null, 2), { flag: 'wx' });
|
|
60
|
+
console.log(JSON.stringify({ status, receiptDir, source, sourceStable: stable, lanes: results.map(({ name, status }) => ({ name, status })) }));
|
|
61
|
+
process.exitCode = status === 'PASS' ? 0 : status === 'UNKNOWN' ? 4 : 1;
|
|
62
|
+
}
|
package/scripts/qe/ux-suite.mjs
CHANGED
|
@@ -11,16 +11,8 @@
|
|
|
11
11
|
// scheduler jitter — it can only be a correctness regression. THIS is a genuine hard gate: a
|
|
12
12
|
// breach here fails the suite, not warns it. See scripts/qe/card-lane-gate.mjs for the full
|
|
13
13
|
// reasoning and the in-process (no subprocess per firing) measurement method.
|
|
14
|
-
// 3.
|
|
15
|
-
//
|
|
16
|
-
// 0.03–0.22ms in-process function against a 250ms budget (~1000x headroom — it can only catch
|
|
17
|
-
// catastrophic regression classes)", while "everything the user actually FEELS — heavy-lane
|
|
18
|
-
// query seconds, session-start WALL TIME, install minutes, dead air, refusal clarity — is
|
|
19
|
-
// advisory or unmeasured". Session-start wall time is the first of those promoted out of tier 1:
|
|
20
|
-
// it is the hook a stranger's Claude Code fires before their first prompt is answered, it is
|
|
21
|
-
// already measured by scripts/selfcheck.mjs's external process-group watchdog (no second timer
|
|
22
|
-
// was written), and its budget is set from a measured distribution — p95 1000ms is ~3.1x the
|
|
23
|
-
// worst measured p95 (323ms over n=110), NOT 1000x. See scripts/qe/session-start-gate.mjs.
|
|
14
|
+
// 3. Automatic hook retirement is checked across declared source registries and pointers.
|
|
15
|
+
// The old SessionStart latency probe remains separately callable for diagnostics.
|
|
24
16
|
//
|
|
25
17
|
// HONESTY (same rules as the product):
|
|
26
18
|
// • Every number is measured on THIS run. Nothing is asserted from memory.
|
|
@@ -39,7 +31,7 @@ import os from 'node:os';
|
|
|
39
31
|
import { fileURLToPath } from 'node:url';
|
|
40
32
|
import { runCommandProbe } from '../../tests/ux/command-probe.mjs';
|
|
41
33
|
import { runCardLaneGate } from './card-lane-gate.mjs';
|
|
42
|
-
import {
|
|
34
|
+
import { automaticHookRetirementStatus } from '../../bin/install.mjs';
|
|
43
35
|
|
|
44
36
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
45
37
|
const RENDER_PROBE = path.resolve(HERE, '../../tests/ux/render-probe.mjs');
|
|
@@ -322,28 +314,11 @@ export async function runUxSuite() {
|
|
|
322
314
|
hardFailures.push(`card-lane latency gate: could not run — ${e.message}`);
|
|
323
315
|
}
|
|
324
316
|
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
const ss = await runSessionStartGate();
|
|
332
|
-
const b = ss.budget;
|
|
333
|
-
const tag = (ok) => (ok ? '✓' : '✗ HARD FAIL');
|
|
334
|
-
console.log(` ${'session-start cold first fire'.padEnd(30)} ${ss.warmupMs.toFixed(0).padStart(10)}ms ${ss.warmupTimedOut ? '✗ HARD FAIL (declared timeout exceeded)' : '✓ inside declared timeout'}`);
|
|
335
|
-
console.log(` ${'session-start p50'.padEnd(30)} ${ss.p50.toFixed(0).padStart(10)}ms (reported, not gated)`);
|
|
336
|
-
console.log(` ${'session-start p95'.padEnd(30)} ${ss.p95.toFixed(0).padStart(10)}ms budget ${b.p95BudgetMs}ms ${tag(ss.p95 <= b.p95BudgetMs)}`);
|
|
337
|
-
console.log(` ${'session-start max'.padEnd(30)} ${ss.max.toFixed(0).padStart(10)}ms absolute-fail ${b.absoluteFailMs}ms ${tag(ss.max <= b.absoluteFailMs)}`);
|
|
338
|
-
console.log(` firings: ${ss.n} sequential fires of the REAL registered command via selfcheck.mjs's watchdog, from ${ss.surface.source}`);
|
|
339
|
-
if (ss.warmupStderr) {
|
|
340
|
-
console.log(` cold trace: ${ss.warmupStderr.trim().split('\n').join(' | ')}`);
|
|
341
|
-
}
|
|
342
|
-
if (!ss.pass) for (const r of ss.reasons) hardFailures.push(`session-start wall time: ${r}`);
|
|
343
|
-
} catch (e) {
|
|
344
|
-
console.log(` ! could not run the session-start wall-time gate: ${e.message}`);
|
|
345
|
-
hardFailures.push(`session-start wall-time gate: could not run — ${e.message}`);
|
|
346
|
-
}
|
|
317
|
+
// Automatic lifecycle hooks are retired; measure registry state, not a fictitious latency.
|
|
318
|
+
const hookRetirement = automaticHookRetirementStatus(path.resolve(HERE, '../..'));
|
|
319
|
+
console.log(`\n Automatic hooks: ${hookRetirement.ok ? 'RETIRED' : 'FAIL'} — ${hookRetirement.files.length} surfaces inspected`);
|
|
320
|
+
if (!hookRetirement.ok) hardFailures.push(...hookRetirement.errors,
|
|
321
|
+
...hookRetirement.registrations.map(row => `automatic registration remains: ${row.file} ${row.event}`));
|
|
347
322
|
|
|
348
323
|
// ── Not run on this host (stated, never faked) ──────────────────────────────────────────────
|
|
349
324
|
console.log('\n ── execution scope ──');
|
|
@@ -363,6 +338,7 @@ export async function runUxSuite() {
|
|
|
363
338
|
budgetsMs: budgets,
|
|
364
339
|
render,
|
|
365
340
|
command: cmd,
|
|
341
|
+
hookRetirement,
|
|
366
342
|
hardFailures,
|
|
367
343
|
pass: hardFailures.length === 0,
|
|
368
344
|
scope: {
|
|
@@ -375,7 +351,7 @@ export async function runUxSuite() {
|
|
|
375
351
|
|
|
376
352
|
console.log('\n ── verdict ──');
|
|
377
353
|
if (hardFailures.length === 0) {
|
|
378
|
-
console.log(' PASS — every probe ran and every render, explanation, dead-air, decision-lane
|
|
354
|
+
console.log(' PASS — every probe ran and every render, explanation, dead-air, decision-lane HARD budget passed; automatic-hook retirement verified.\n');
|
|
379
355
|
return receipt;
|
|
380
356
|
}
|
|
381
357
|
console.log(' FAIL (hard):');
|
|
@@ -0,0 +1,124 @@
|
|
|
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 crypto from 'node:crypto';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { extractZip } from '../kb/zip-extract.mjs';
|
|
9
|
+
import { evaluateCandidateReceipt } from './release-proof.mjs';
|
|
10
|
+
import { aggregateEvidence } from './release-evidence-aggregate.mjs';
|
|
11
|
+
import { payloadIdFor } from './release-payload.mjs';
|
|
12
|
+
import { canonicalJson } from './release-transaction.mjs';
|
|
13
|
+
|
|
14
|
+
const REPO = 'stuinfla/ruvnet-brain';
|
|
15
|
+
const WORKFLOW = '.github/workflows/release-candidate-preflight.yml';
|
|
16
|
+
const positive = (n) => Number.isSafeInteger(n) && n > 0;
|
|
17
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
18
|
+
export function trustedQualificationRun(run, workflow, sha) {
|
|
19
|
+
return positive(run?.id) && run.workflow_id === workflow.id && run.path === WORKFLOW
|
|
20
|
+
&& run.event === 'push' && run.head_sha === sha && /^release\/.+/.test(run.head_branch || '')
|
|
21
|
+
&& positive(run.repository?.id) && run.repository.full_name === REPO
|
|
22
|
+
&& run.head_repository?.id === run.repository.id && run.head_repository.full_name === REPO;
|
|
23
|
+
}
|
|
24
|
+
function trustedArtifact(artifact, run, name) {
|
|
25
|
+
return positive(artifact?.id) && artifact.name === name && artifact.expired === false
|
|
26
|
+
&& /^sha256:[a-f0-9]{64}$/.test(artifact.digest || '') && positive(artifact.size_in_bytes)
|
|
27
|
+
&& artifact.workflow_run?.id === run.id && artifact.workflow_run.head_sha === run.head_sha
|
|
28
|
+
&& artifact.workflow_run.head_branch === run.head_branch
|
|
29
|
+
&& artifact.workflow_run.repository_id === run.repository.id
|
|
30
|
+
&& artifact.workflow_run.head_repository_id === run.repository.id;
|
|
31
|
+
}
|
|
32
|
+
export function validateQualificationReceipt(receipt, { sha, run, artifact, requiredCheck }) {
|
|
33
|
+
if (!['canonical-qa', 'integration'].includes(requiredCheck)) throw new Error('unsupported required check');
|
|
34
|
+
if (receipt?.schemaVersion !== 1 || receipt.kind !== 'ruvnet-brain-qualified-candidate'
|
|
35
|
+
|| receipt.sha !== sha || receipt.runId !== run.id
|
|
36
|
+
|| canonicalJson(receipt.artifact) !== canonicalJson({ id: artifact.id, name: artifact.name,
|
|
37
|
+
digest: artifact.digest, size: artifact.size_in_bytes })) throw new Error('qualification receipt provenance differs');
|
|
38
|
+
const candidate = receipt.candidateReceipt;
|
|
39
|
+
if (evaluateCandidateReceipt(candidate).verdict !== 'PASS' || candidate.sha !== sha) throw new Error('candidate seal failed');
|
|
40
|
+
const manifest = receipt.payloadManifest;
|
|
41
|
+
if (manifest?.schemaVersion !== 1 || manifest.candidateSha !== sha || manifest.version !== candidate.version
|
|
42
|
+
|| manifest.tag !== candidate.tag || !Array.isArray(manifest.members) || !manifest.members.length
|
|
43
|
+
|| new Set(manifest.members.map(({ name }) => name)).size !== manifest.members.length
|
|
44
|
+
|| manifest.members.some((m) => typeof m.name !== 'string' || path.basename(m.name) !== m.name
|
|
45
|
+
|| !positive(m.size) || !/^[a-f0-9]{64}$/.test(m.sha256 || ''))) throw new Error('payload manifest identity differs');
|
|
46
|
+
const packageName = String(candidate.artifact.path).replaceAll('\\', '/').split('/').at(-1);
|
|
47
|
+
if (!manifest.members.some((m) => m.name === packageName
|
|
48
|
+
&& m.sha256 === String(candidate.artifact.sha256).replace(/^sha256:/, ''))) throw new Error('candidate package is outside payload');
|
|
49
|
+
const envelope = receipt.aggregateEnvelope;
|
|
50
|
+
const rebuilt = aggregateEvidence({ sha, payloadId: payloadIdFor(manifest), leaves: envelope?.leaves });
|
|
51
|
+
if (canonicalJson(rebuilt) !== canonicalJson(envelope)
|
|
52
|
+
|| envelope.leaves.some((leaf) => leaf.runId !== run.id)) throw new Error('aggregate evidence digest or run differs');
|
|
53
|
+
return { verdict: 'PASS', sha, requiredCheck, runId: run.id, artifactId: artifact.id,
|
|
54
|
+
payloadId: rebuilt.payloadId, evidenceDigest: rebuilt.evidenceDigest };
|
|
55
|
+
}
|
|
56
|
+
const api = (endpoint) => JSON.parse(execFileSync('gh', ['api', `repos/${REPO}/${endpoint}`], {
|
|
57
|
+
encoding: 'utf8', timeout: 30000, maxBuffer: 16 * 1024 * 1024 }));
|
|
58
|
+
export async function downloadQualificationReceipt(artifact, fetchArchive = (id) => execFileSync('gh',
|
|
59
|
+
['api', `repos/${REPO}/actions/artifacts/${id}/zip`], { timeout: 60000, maxBuffer: 2 * 1024 * 1024 })) {
|
|
60
|
+
if (artifact.size_in_bytes > 2 * 1024 * 1024) throw new Error('qualification receipt artifact oversized');
|
|
61
|
+
const bytes = await fetchArchive(artifact.id);
|
|
62
|
+
if (`sha256:${sha256(bytes)}` !== artifact.digest) throw new Error('qualification artifact digest differs');
|
|
63
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'qualified-candidate-'));
|
|
64
|
+
try {
|
|
65
|
+
const zip = path.join(temp, 'receipt.zip');
|
|
66
|
+
fs.writeFileSync(zip, bytes);
|
|
67
|
+
const out = path.join(temp, 'contents');
|
|
68
|
+
const extracted = await extractZip(zip, out);
|
|
69
|
+
if (canonicalJson(extracted.entryNames) !== canonicalJson(['qualification-receipt.json'])) throw new Error('unexpected qualification artifact members');
|
|
70
|
+
const file = path.join(out, 'qualification-receipt.json');
|
|
71
|
+
if (fs.statSync(file).size > 1024 * 1024) throw new Error('qualification receipt oversized');
|
|
72
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
73
|
+
} finally { fs.rmSync(temp, { recursive: true, force: true }); }
|
|
74
|
+
}
|
|
75
|
+
export async function checkQualifiedCandidate({ sha, requiredCheck, timeoutMs = 7200000, pollMs = 30000,
|
|
76
|
+
readApi = api, readReceipt = downloadQualificationReceipt, now = Date.now,
|
|
77
|
+
pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) } = {}) {
|
|
78
|
+
if (!/^[a-f0-9]{40}$/.test(sha || '') || !['canonical-qa', 'integration'].includes(requiredCheck)
|
|
79
|
+
|| !positive(timeoutMs) || timeoutMs > 7200000 || !positive(pollMs) || pollMs < 30000 || pollMs > 60000) throw new Error('invalid bounded qualification request');
|
|
80
|
+
const workflow = await readApi('actions/workflows/release-candidate-preflight.yml');
|
|
81
|
+
if (!positive(workflow.id) || workflow.path !== WORKFLOW) throw new Error('untrusted qualification workflow');
|
|
82
|
+
const deadline = now() + timeoutMs;
|
|
83
|
+
for (;;) {
|
|
84
|
+
const list = await readApi(`actions/workflows/${workflow.id}/runs?event=push&head_sha=${sha}&per_page=100`);
|
|
85
|
+
if (list.total_count > 100) throw new Error('qualification run listing truncated');
|
|
86
|
+
const selected = (list.workflow_runs || []).filter((r) => trustedQualificationRun(r, workflow, sha)).sort((a,b) => b.id-a.id)[0];
|
|
87
|
+
if (selected) {
|
|
88
|
+
const run = await readApi(`actions/runs/${selected.id}`);
|
|
89
|
+
if (run.id !== selected.id || !trustedQualificationRun(run, workflow, sha)) throw new Error('qualification run provenance changed');
|
|
90
|
+
if (run.status === 'completed') {
|
|
91
|
+
if (run.conclusion !== 'success') throw new Error(`qualification failed: ${run.conclusion}`);
|
|
92
|
+
const listed = await readApi(`actions/runs/${run.id}/artifacts?per_page=100`);
|
|
93
|
+
if (listed.total_count > 100) throw new Error('qualification artifact listing truncated');
|
|
94
|
+
const select = async (name) => {
|
|
95
|
+
const rows = (listed.artifacts || []).filter((a) => a.name === name);
|
|
96
|
+
if (rows.length !== 1) throw new Error('missing or duplicate qualification artifact');
|
|
97
|
+
const artifact = await readApi(`actions/artifacts/${rows[0].id}`);
|
|
98
|
+
if (artifact.id !== rows[0].id || !trustedArtifact(artifact, run, name)) throw new Error('qualification artifact provenance changed');
|
|
99
|
+
return artifact;
|
|
100
|
+
};
|
|
101
|
+
const artifact = await select(`release-candidate-${sha}`);
|
|
102
|
+
const small = await select(`qualification-receipt-${sha}`);
|
|
103
|
+
return validateQualificationReceipt(await readReceipt(small), { sha, run, artifact, requiredCheck });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const remaining = deadline - now();
|
|
107
|
+
if (remaining <= 0) throw new Error('timed out waiting for exact-SHA qualification');
|
|
108
|
+
process.stderr.write('Waiting for exact-SHA release qualification\n');
|
|
109
|
+
await pause(Math.min(pollMs, remaining));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
113
|
+
try {
|
|
114
|
+
if (process.env.GITHUB_REPOSITORY !== REPO) throw new Error('unexpected qualification repository');
|
|
115
|
+
const args = process.argv.slice(2), options = {};
|
|
116
|
+
for (let i=0;i<args.length;i+=2) {
|
|
117
|
+
if (!['--sha','--required-check','--timeout-ms','--poll-ms'].includes(args[i]) || !args[i+1]) throw new Error('invalid qualification arguments');
|
|
118
|
+
options[args[i]] = args[i+1];
|
|
119
|
+
}
|
|
120
|
+
console.log(JSON.stringify(await checkQualifiedCandidate({ sha: options['--sha'], requiredCheck: options['--required-check'],
|
|
121
|
+
...(options['--timeout-ms'] ? {timeoutMs:Number(options['--timeout-ms'])} : {}),
|
|
122
|
+
...(options['--poll-ms'] ? {pollMs:Number(options['--poll-ms'])} : {}) })));
|
|
123
|
+
} catch(error) { console.error(error.message); process.exitCode=1; }
|
|
124
|
+
}
|
|
@@ -34,7 +34,7 @@ import fs from 'node:fs';
|
|
|
34
34
|
import os from 'node:os';
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
import {
|
|
37
|
-
RECEIPT_PREFIX,
|
|
37
|
+
RECEIPT_PREFIX, isClosedReceipt, ALLOWED_TRANSITIONS, canonicalJson, digestReceipt, signReceipt,
|
|
38
38
|
} from './release-transaction.mjs';
|
|
39
39
|
|
|
40
40
|
const REPO = 'stuinfla/ruvnet-brain';
|
|
@@ -65,7 +65,7 @@ const receipts = receiptAssets.map(fetchAsset).sort((a, b) => a.sequence - b.seq
|
|
|
65
65
|
const last = receipts.at(-1);
|
|
66
66
|
log(`${TAG}: ${receipts.length} receipt(s), latest seq=${last.sequence} state=${last.state} txn=${String(last.transactionId).slice(0, 16)}…`);
|
|
67
67
|
|
|
68
|
-
if (
|
|
68
|
+
if (isClosedReceipt(last)) {
|
|
69
69
|
log(`already terminal (${last.state}) — nothing to do.`);
|
|
70
70
|
process.exit(0);
|
|
71
71
|
}
|
|
@@ -6,7 +6,7 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { digest, generationLedgerBytes, projectPublicGenerationLedger,
|
|
9
|
-
releaseCoverageGenerationFor } from '../plugin/scripts/coverage-integrity.mjs';
|
|
9
|
+
releaseCoverageGenerationFor, validateCoverageDirectory } from '../plugin/scripts/coverage-integrity.mjs';
|
|
10
10
|
import { validatePublicInventory } from './public-inventory.mjs';
|
|
11
11
|
import { readRvfGenerations } from './rvf-generation.mjs';
|
|
12
12
|
|
|
@@ -15,6 +15,25 @@ const arg = (name, fallback = null) => { const i = process.argv.indexOf(name); r
|
|
|
15
15
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
16
16
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
17
17
|
|
|
18
|
+
// The first assembly supplies seed bytes to the projection producer. Only the final, projected
|
|
19
|
+
// assembly has a release-bound public ledger; its runtime ledger must describe that same set.
|
|
20
|
+
export function bindAssembledReleaseProjection({ assetsDir, version, sourceSnapshot }) {
|
|
21
|
+
const ledger = readJson(path.join(assetsDir, 'PUBLIC-RVF-GENERATIONS.json'));
|
|
22
|
+
if (!/^[a-f0-9]{40}$/.test(sourceSnapshot || '') || ledger.schemaVersion !== 2
|
|
23
|
+
|| ledger.kind !== 'ruvnet-brain-public-generation-ledger'
|
|
24
|
+
|| ledger.brainVersion !== version || ledger.releaseTag !== `v${version}`
|
|
25
|
+
|| ledger.sourceSnapshot !== sourceSnapshot) {
|
|
26
|
+
throw new Error('assembled public ledger does not bind this release source');
|
|
27
|
+
}
|
|
28
|
+
fs.writeFileSync(path.join(assetsDir, 'RVF-GENERATIONS.json'), `${JSON.stringify({
|
|
29
|
+
...ledger, kind: 'ruvnet-brain-runtime-generation-ledger',
|
|
30
|
+
}, null, 2)}\n`);
|
|
31
|
+
const result = validateCoverageDirectory(assetsDir, { expectedVersion: version,
|
|
32
|
+
expectedSourceSnapshot: sourceSnapshot, requireCompleteProfile: true });
|
|
33
|
+
if (!result.valid) throw new Error(`assembled release projection rejected: ${result.failures.join('; ')}`);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
function seedCompatibleGistReceipt() {
|
|
19
38
|
const source = readJson(path.join(ROOT, 'kb', 'ruv-gists.sources.json'));
|
|
20
39
|
const gists = {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Reviewed stabilization release boundaries. Legacy tests remain developer diagnostics.
|
|
2
|
+
export const RELEASE_REQUIREMENTS = Object.freeze({
|
|
3
|
+
"source": [
|
|
4
|
+
{
|
|
5
|
+
"id": "signed-artifacts",
|
|
6
|
+
"reason": "Signature verification and exact assembled coverage reject changed bytes",
|
|
7
|
+
"files": [
|
|
8
|
+
"tests/unit/sign-verify-roundtrip.test.mjs",
|
|
9
|
+
"tests/unit/assembled-release-projection.test.mjs"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "protected-publication",
|
|
14
|
+
"reason": "Only owner-authorized exact-source artifacts enter publication",
|
|
15
|
+
"files": [
|
|
16
|
+
"tests/unit/protected-release-invocation.test.mjs",
|
|
17
|
+
"tests/unit/release-identity-invariants.test.mjs",
|
|
18
|
+
"tests/unit/release-transaction.test.mjs",
|
|
19
|
+
"tests/unit/prepublication-evidence.test.mjs",
|
|
20
|
+
"tests/unit/integration-evidence.test.mjs",
|
|
21
|
+
"tests/unit/qualified-candidate-check.test.mjs",
|
|
22
|
+
"tests/unit/release-qualification.test.mjs",
|
|
23
|
+
"tests/unit/development-push-boundary.test.mjs"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "public-verification",
|
|
28
|
+
"reason": "Public bytes, native update evidence and terminal receipts are bound to the actual candidate and run",
|
|
29
|
+
"files": [
|
|
30
|
+
"tests/unit/public-verification-aggregate.test.mjs",
|
|
31
|
+
"tests/unit/public-verification-finalizer.test.mjs",
|
|
32
|
+
"tests/unit/public-verification-lane.test.mjs",
|
|
33
|
+
"tests/unit/public-verification-abandon.test.mjs",
|
|
34
|
+
"tests/unit/publication-receipt-producer.test.mjs",
|
|
35
|
+
"tests/unit/recovery-candidate-source.test.mjs"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "safe-install-update",
|
|
40
|
+
"reason": "Installation and update preserve prior and private state and reject invalid dependencies",
|
|
41
|
+
"files": [
|
|
42
|
+
"tests/unit/automatic-hook-retirement.test.mjs",
|
|
43
|
+
"tests/unit/install-activation-rollback.test.mjs",
|
|
44
|
+
"tests/unit/forge-update-apply-rollback.test.mjs"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "native-scheduler",
|
|
49
|
+
"reason": "Owned scheduler lifecycle, execution identity, measured no-op and signed installed coverage",
|
|
50
|
+
"files": [
|
|
51
|
+
"tests/unit/nightly-scheduler.test.mjs",
|
|
52
|
+
"tests/unit/nightly-refresh-launcher.test.mjs",
|
|
53
|
+
"tests/unit/nightly-two-run-proof.test.mjs"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"integration": [
|
|
58
|
+
{
|
|
59
|
+
"id": "native-explicit-interface",
|
|
60
|
+
"reason": "Real MCP subprocess readiness, command policy and literal argv safety",
|
|
61
|
+
"files": [
|
|
62
|
+
"tests/integration/managed-cli-mcp.test.mjs"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "private-bundle-boundary",
|
|
67
|
+
"reason": "Actual bundle builder rejects absent private fence and incomplete public payloads",
|
|
68
|
+
"files": [
|
|
69
|
+
"tests/integration/build-bundle-fence.test.mjs"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "native-codex-discovery",
|
|
74
|
+
"reason": "Actual Codex plugin discovery and repair preserve explicit disabled state",
|
|
75
|
+
"files": [
|
|
76
|
+
"tests/integration/codex-skill-discovery.test.mjs"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "owned-uninstall",
|
|
81
|
+
"reason": "Actual offline uninstall preserves unrelated files and user guidance",
|
|
82
|
+
"files": [
|
|
83
|
+
"tests/integration/uninstall-footprint.test.mjs"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
});
|