ruvnet-brain 4.0.1 → 4.0.4
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/.claude-plugin/marketplace.json +1 -0
- package/README.md +4 -4
- package/bin/install.mjs +303 -24
- package/console/CONTRACT.md +172 -0
- package/console/activity.js +753 -0
- package/console/app.js +4189 -0
- package/console/architecture.html +1221 -0
- package/console/assets/depth-1.webp +0 -0
- package/console/assets/depth-2.webp +0 -0
- package/console/assets/depth-3.webp +0 -0
- package/console/assets/harness-vs-plain.svg +259 -0
- package/console/assets/hero.webp +0 -0
- package/console/assets/memory.webp +0 -0
- package/console/assets/metaharness.svg +247 -0
- package/console/index.html +777 -0
- package/console/install-architecture.html +162 -0
- package/console/install-mockup.html +543 -0
- package/console/style.css +2144 -0
- package/console/tips.css +926 -0
- package/console/tips.html +858 -0
- package/console/tips.js +128 -0
- package/docs/RELEASE-NOTES-4.0.md +88 -0
- package/kb/model-requirements.mjs +37 -6
- package/keys/ruvnet-brain-signing.pub.pem +3 -0
- package/package.json +8 -22
- package/plugin/.claude-plugin/marketplace.json +1 -0
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +2 -2
- package/plugin/commands/configure.md +3 -2
- package/plugin/commands/rvbc.md +4 -3
- package/plugin/commands/rvcb.md +2 -2
- package/plugin/commands/whats-new.md +6 -6
- package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
- package/plugin/hooks/hooks.json +1 -2
- package/plugin/mcp/managed-cli-interface.mjs +47 -4
- package/plugin/mcp/server.mjs +90 -32
- package/plugin/scripts/detach.mjs +14 -0
- package/plugin/scripts/first-session-worker.mjs +38 -0
- package/plugin/scripts/ground-ruvnet.sh +16 -6
- package/plugin/scripts/hook-shim.mjs +34 -29
- package/plugin/scripts/learn-capture.sh +22 -3
- package/plugin/scripts/learn-flush.mjs +21 -4
- package/plugin/scripts/runtime-preferences.mjs +269 -0
- package/plugin/scripts/session-start-core.mjs +503 -0
- package/plugin/scripts/session-start.sh +3 -858
- package/plugin/scripts/whats-new.mjs +42 -0
- package/plugin/skills/brain-console/SKILL.md +4 -2
- package/plugin/skills/release-proof/SKILL.md +98 -0
- package/plugin/skills/release-proof/agents/openai.yaml +4 -0
- package/plugin/skills/release-proof/references/receipt-contract.md +44 -0
- package/plugin/skills/release-proof/scripts/release-proof.mjs +286 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
- package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
- package/plugin/skills/rvbc/SKILL.md +9 -6
- package/plugin/skills/whats-new/SKILL.md +4 -4
- package/scripts/adr-backfill.mjs +107 -0
- package/scripts/advocacy-outcomes.mjs +808 -0
- package/scripts/agentdb-context.mjs +216 -0
- package/scripts/agentdb-fleet-doctor.mjs +101 -0
- package/scripts/ascii-drift.mjs +236 -0
- package/scripts/behavioral-l1-l4.mjs +210 -0
- package/scripts/brain-capability-check.mjs +72 -0
- package/scripts/brain-grade-groundtruth.mjs +100 -0
- package/scripts/brain-latency-50.mjs +227 -0
- package/scripts/brain-novice-50.mjs +189 -0
- package/scripts/brain-stamp.mjs +94 -0
- package/scripts/brain-state.mjs +212 -0
- package/scripts/build-bundle.mjs +531 -0
- package/scripts/build-concepts.mjs +132 -0
- package/scripts/build-l2.mjs +71 -0
- package/scripts/build-primer.mjs +73 -0
- package/scripts/build-symbols.mjs +68 -0
- package/scripts/calibrate-router.mjs +97 -0
- package/scripts/capability-audit.mjs +321 -0
- package/scripts/capability-registry.mjs +876 -0
- package/scripts/check-indexation.mjs +108 -0
- package/scripts/check-legibility.mjs +189 -0
- package/scripts/ci/build-fixture-kb.mjs +67 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
- package/scripts/ci/learning-replay-recorder.mjs +59 -0
- package/scripts/ci/mutate-hook-timeout.mjs +70 -0
- package/scripts/ci/stranger-fixture-stage.mjs +17 -0
- package/scripts/ci/stranger-scenario.mjs +228 -0
- package/scripts/ci/stranger-timeout.mjs +25 -0
- package/scripts/ci-verdict.mjs +29 -0
- package/scripts/claims-verify.mjs +710 -0
- package/scripts/clear-claude-tmp.sh +31 -0
- package/scripts/console-engine.mjs +434 -0
- package/scripts/console-engine.test.mjs +125 -0
- package/scripts/corpus-qa.mjs +250 -0
- package/scripts/correction-detect-embed.mjs +346 -0
- package/scripts/correction-detect-measure.mjs +270 -0
- package/scripts/correction-detect.mjs +686 -0
- package/scripts/count-chunks.mjs +54 -0
- package/scripts/described-questions.json +30 -0
- package/scripts/design-grade.mjs +58 -0
- package/scripts/dev-plugin-link.sh +105 -0
- package/scripts/distill-project.mjs +200 -0
- package/scripts/doc-currency.mjs +801 -0
- package/scripts/eval-brain.mjs +244 -0
- package/scripts/fix-metaharness-memretrieve.mjs +121 -0
- package/scripts/fix-workstream.mjs +291 -0
- package/scripts/full-hints.mjs +87 -0
- package/scripts/gate.sh +39 -0
- package/scripts/gates.mjs +146 -0
- package/scripts/gen-console-images.mjs +54 -0
- package/scripts/gen-images.mjs +47 -0
- package/scripts/git-clone-refresh.mjs +52 -0
- package/scripts/git-hooks/pre-push +126 -0
- package/scripts/goal-match.mjs +398 -0
- package/scripts/goldie-research.mjs +223 -0
- package/scripts/goldie-weekly.sh +67 -0
- package/scripts/health-repair.mjs +237 -0
- package/scripts/helix-scenario-questions.json +10 -0
- package/scripts/ingest-gists.mjs +230 -0
- package/scripts/ingest-meeting.mjs +115 -0
- package/scripts/ingest-repo.mjs +79 -0
- package/scripts/install-npx-witness.sh +49 -0
- package/scripts/issue-fix.mjs +558 -0
- package/scripts/issue-watch.mjs +276 -0
- package/scripts/issue4-close-note.md +31 -0
- package/scripts/key-canary.mjs +91 -0
- package/scripts/latency-to-surface.mjs +233 -0
- package/scripts/learning-enable.mjs +380 -0
- package/scripts/learning-replay.mjs +1570 -0
- package/scripts/learnings.mjs +62 -0
- package/scripts/lesson-gate.mjs +680 -0
- package/scripts/lesson-lifecycle.mjs +449 -0
- package/scripts/lesson-promote.mjs +262 -0
- package/scripts/lesson-ratify.mjs +98 -0
- package/scripts/lesson-seed.mjs +252 -0
- package/scripts/lesson-store.mjs +447 -0
- package/scripts/loop-checkpoint.mjs +86 -0
- package/scripts/memdb-health.sh +14 -0
- package/scripts/memory-doctor.mjs +326 -0
- package/scripts/model-catalog.mjs +79 -0
- package/scripts/nightly-controller.mjs +66 -0
- package/scripts/nightly-gists.sh +72 -0
- package/scripts/nightly-wrapper.sh +172 -0
- package/scripts/notify.sh +12 -0
- package/scripts/npx-witness.sh +56 -0
- package/scripts/onboarding-console.mjs +2922 -0
- package/scripts/private-fence.mjs +69 -0
- package/scripts/proactivity-metrics.mjs +118 -0
- package/scripts/proof-questions.json +56 -0
- package/scripts/protected-release-invocation.mjs +76 -0
- package/scripts/prove.mjs +95 -0
- package/scripts/proxy/claude-proxied.sh +57 -0
- package/scripts/proxy/proxy-revert.sh +59 -0
- package/scripts/proxy/proxy-up.sh +60 -0
- package/scripts/proxy/proxy-verify.mjs +142 -0
- package/scripts/publication-receipt.mjs +307 -0
- package/scripts/published-surface-probe.mjs +241 -0
- package/scripts/qe/card-lane-gate.mjs +162 -0
- package/scripts/qe/session-start-gate.mjs +229 -0
- package/scripts/qe/ux-suite.mjs +323 -0
- package/scripts/reconcile-project.mjs +0 -0
- package/scripts/record-lesson.mjs +113 -0
- package/scripts/refresh-model-catalog.mjs +99 -0
- package/scripts/release-authority.mjs +93 -0
- package/scripts/release-proof.mjs +9 -0
- package/scripts/release-vector.mjs +281 -0
- package/scripts/release.mjs +439 -0
- package/scripts/remedy-registry.mjs +247 -0
- package/scripts/rerank-cap-eval.mjs +265 -0
- package/scripts/rerank-cap-warm-ab.mjs +129 -0
- package/scripts/route-cheap.mjs +20 -15
- package/scripts/router-utilization.mjs +182 -0
- package/scripts/routing-flywheel.mjs +596 -0
- package/scripts/rvf-generation.mjs +104 -0
- package/scripts/rvf-index-audit.mjs +138 -0
- package/scripts/self-update.mjs +296 -0
- package/scripts/selfcheck.mjs +7 -1
- package/scripts/sign-bundle.mjs +69 -0
- package/scripts/signal-watch.mjs +171 -0
- package/scripts/stabilization-receipt.mjs +108 -0
- package/scripts/stack-sync.mjs +469 -0
- package/scripts/stamp-existing-rvf-generations.mjs +53 -0
- package/scripts/stamp-sweep.mjs +144 -0
- package/scripts/status-honesty.mjs +102 -0
- package/scripts/sync-version.mjs +217 -0
- package/scripts/token-report.mjs +102 -0
- package/scripts/top100-benchmark.mjs +479 -0
- package/scripts/top100-corpus.mjs +112 -0
- package/scripts/top100-semantic-assertions.mjs +449 -0
- package/scripts/update-apply.mjs +9 -0
- package/scripts/upgrade-notice.mjs +14 -0
- package/scripts/verify-bundle.mjs +51 -0
- package/scripts/verify-channels.mjs +184 -0
- package/scripts/verify-model-catalog.mjs +104 -0
- package/scripts/verify-nightly-close-issue4.sh +31 -0
- package/scripts/version.mjs +40 -0
- package/scripts/wired-check.mjs +867 -0
- package/plugin/scripts/finalize-token-meter.mjs +0 -25
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// rvf-generation.mjs — bind each canonical RVF byte generation to the Brain product version.
|
|
2
|
+
//
|
|
3
|
+
// @ruvector/rvf's Node API does not currently expose a supported custom-manifest writer for an
|
|
4
|
+
// existing store. This checksum-bound companion is therefore the fail-closed identity record:
|
|
5
|
+
// changing even one RVF byte changes sha256, while brainVersion/releaseTag identify the npm and
|
|
6
|
+
// GitHub release that owns those exact bytes.
|
|
7
|
+
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { getVersion, getVersionTag } from './version.mjs';
|
|
12
|
+
|
|
13
|
+
export const RVF_GENERATIONS_FILE = 'RVF-GENERATIONS.json';
|
|
14
|
+
|
|
15
|
+
export function canonicalRvfStores(dir) {
|
|
16
|
+
return fs.readdirSync(dir)
|
|
17
|
+
.map((file) => file.match(/^(.+)\.big\.rvf$/)?.[1])
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.sort();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasCanonicalRvfStore(dir, name) {
|
|
23
|
+
const wanted = String(name).toLowerCase();
|
|
24
|
+
return canonicalRvfStores(dir).some((store) => store.toLowerCase() === wanted);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sha256File(file) {
|
|
28
|
+
const hash = crypto.createHash('sha256');
|
|
29
|
+
const fd = fs.openSync(file, 'r');
|
|
30
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
31
|
+
try {
|
|
32
|
+
let bytes;
|
|
33
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) {
|
|
34
|
+
hash.update(buffer.subarray(0, bytes));
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
fs.closeSync(fd);
|
|
38
|
+
}
|
|
39
|
+
return hash.digest('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readRvfGenerations(dir) {
|
|
43
|
+
const file = path.join(dir, RVF_GENERATIONS_FILE);
|
|
44
|
+
if (!fs.existsSync(file)) {
|
|
45
|
+
return { schemaVersion: 1, brainVersion: getVersion(), releaseTag: getVersionTag(), stores: {} };
|
|
46
|
+
}
|
|
47
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
48
|
+
parsed.stores ||= {};
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function writeRvfGeneration({
|
|
53
|
+
dir,
|
|
54
|
+
store,
|
|
55
|
+
rvfFile = `${store}.big.rvf`,
|
|
56
|
+
model,
|
|
57
|
+
dimensions,
|
|
58
|
+
sourceCommit = null,
|
|
59
|
+
builtUtc = new Date().toISOString(),
|
|
60
|
+
previousDir = dir,
|
|
61
|
+
}) {
|
|
62
|
+
const rvfPath = path.join(dir, rvfFile);
|
|
63
|
+
if (!fs.existsSync(rvfPath)) throw new Error(`cannot stamp missing RVF: ${rvfPath}`);
|
|
64
|
+
const manifest = readRvfGenerations(previousDir);
|
|
65
|
+
manifest.schemaVersion = 1;
|
|
66
|
+
manifest.brainVersion = getVersion();
|
|
67
|
+
manifest.releaseTag = getVersionTag();
|
|
68
|
+
manifest.stores[store] = {
|
|
69
|
+
file: rvfFile,
|
|
70
|
+
sha256: sha256File(rvfPath),
|
|
71
|
+
bytes: fs.statSync(rvfPath).size,
|
|
72
|
+
model,
|
|
73
|
+
dimensions,
|
|
74
|
+
sourceCommit,
|
|
75
|
+
builtUtc,
|
|
76
|
+
};
|
|
77
|
+
fs.writeFileSync(path.join(dir, RVF_GENERATIONS_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
78
|
+
return manifest.stores[store];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function verifyRvfGenerations(dir, {
|
|
82
|
+
version = getVersion(),
|
|
83
|
+
releaseTag = getVersionTag(),
|
|
84
|
+
requiredStores = [],
|
|
85
|
+
allowMissingFiles = false,
|
|
86
|
+
} = {}) {
|
|
87
|
+
const manifest = readRvfGenerations(dir);
|
|
88
|
+
const failures = [];
|
|
89
|
+
if (manifest.brainVersion !== version) failures.push(`brainVersion=${manifest.brainVersion}, expected ${version}`);
|
|
90
|
+
if (manifest.releaseTag !== releaseTag) failures.push(`releaseTag=${manifest.releaseTag}, expected ${releaseTag}`);
|
|
91
|
+
for (const store of requiredStores) {
|
|
92
|
+
if (!manifest.stores[store]) failures.push(`${store}: no generation record`);
|
|
93
|
+
}
|
|
94
|
+
for (const [store, generation] of Object.entries(manifest.stores)) {
|
|
95
|
+
const file = path.join(dir, generation.file || `${store}.big.rvf`);
|
|
96
|
+
if (!fs.existsSync(file)) {
|
|
97
|
+
if (!allowMissingFiles) failures.push(`${store}: missing ${path.basename(file)}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const actual = sha256File(file);
|
|
101
|
+
if (actual !== generation.sha256) failures.push(`${store}: sha256=${actual}, recorded ${generation.sha256}`);
|
|
102
|
+
}
|
|
103
|
+
return { manifest, failures };
|
|
104
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { loadRvf, closeReadonlyRvf } from '../kb/resolve-deps.mjs';
|
|
6
|
+
import { persistAndVerifyRvfIndex } from '../kb/rvf-index.mjs';
|
|
7
|
+
import {
|
|
8
|
+
readRvfGenerations,
|
|
9
|
+
sha256File,
|
|
10
|
+
writeRvfGeneration,
|
|
11
|
+
} from './rvf-generation.mjs';
|
|
12
|
+
|
|
13
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
export const RVF_HNSW_THRESHOLD = 1024;
|
|
15
|
+
|
|
16
|
+
function dimensionsFor(rvfPath) {
|
|
17
|
+
const embedPath = `${rvfPath}.embed.json`;
|
|
18
|
+
const config = JSON.parse(fs.readFileSync(embedPath, 'utf8'));
|
|
19
|
+
const dimensions = Number(config.dimensions);
|
|
20
|
+
if (!Number.isInteger(dimensions) || dimensions < 1) {
|
|
21
|
+
throw new Error(`invalid dimensions in ${embedPath}`);
|
|
22
|
+
}
|
|
23
|
+
return dimensions;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function inspectRvfIndex(rvfPath, RvfDatabase) {
|
|
27
|
+
const db = await RvfDatabase.openReadonly(rvfPath);
|
|
28
|
+
try {
|
|
29
|
+
const [status, segments] = await Promise.all([db.status(), db.segments()]);
|
|
30
|
+
const hasIndex = segments.some(({ segType }) => segType === 'index');
|
|
31
|
+
return {
|
|
32
|
+
path: rvfPath,
|
|
33
|
+
totalVectors: status.totalVectors,
|
|
34
|
+
indexRequired: status.totalVectors >= RVF_HNSW_THRESHOLD,
|
|
35
|
+
hasIndex,
|
|
36
|
+
state: status.totalVectors >= RVF_HNSW_THRESHOLD && !hasIndex ? 'FAIL' : 'PASS',
|
|
37
|
+
};
|
|
38
|
+
} finally {
|
|
39
|
+
await closeReadonlyRvf(db);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function auditRvfIndexes(rvfPaths) {
|
|
44
|
+
const { mod: { RvfDatabase } } = loadRvf();
|
|
45
|
+
return Promise.all(rvfPaths.map((rvfPath) => inspectRvfIndex(rvfPath, RvfDatabase)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function repairRvfIndex(rvfPath, RvfDatabase) {
|
|
49
|
+
const before = await inspectRvfIndex(rvfPath, RvfDatabase);
|
|
50
|
+
if (before.state === 'PASS') return { ...before, repaired: false, elapsedMs: 0 };
|
|
51
|
+
|
|
52
|
+
const started = Date.now();
|
|
53
|
+
const db = await RvfDatabase.open(rvfPath);
|
|
54
|
+
const proof = await persistAndVerifyRvfIndex({
|
|
55
|
+
db,
|
|
56
|
+
dimensions: dimensionsFor(rvfPath),
|
|
57
|
+
rvfPath,
|
|
58
|
+
RvfDatabase,
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
...before,
|
|
62
|
+
hasIndex: proof.hasIndex,
|
|
63
|
+
state: proof.hasIndex ? 'PASS' : 'FAIL',
|
|
64
|
+
repaired: proof.hasIndex,
|
|
65
|
+
elapsedMs: Date.now() - started,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function restampChangedRvfGenerations(kbDir, results) {
|
|
70
|
+
const ledger = readRvfGenerations(kbDir);
|
|
71
|
+
let stamped = 0;
|
|
72
|
+
for (const result of results) {
|
|
73
|
+
if (result.state !== 'PASS' || !result.path.endsWith('.big.rvf')) continue;
|
|
74
|
+
const store = path.basename(result.path, '.big.rvf');
|
|
75
|
+
const prior = ledger.stores?.[store];
|
|
76
|
+
if (!prior || prior.sha256 === sha256File(result.path)) continue;
|
|
77
|
+
const embed = JSON.parse(fs.readFileSync(`${result.path}.embed.json`, 'utf8'));
|
|
78
|
+
writeRvfGeneration({
|
|
79
|
+
dir: kbDir,
|
|
80
|
+
store,
|
|
81
|
+
rvfFile: path.basename(result.path),
|
|
82
|
+
model: prior.model || embed.model || null,
|
|
83
|
+
dimensions: prior.dimensions || Number(embed.dimensions),
|
|
84
|
+
sourceCommit: prior.sourceCommit ?? null,
|
|
85
|
+
});
|
|
86
|
+
stamped++;
|
|
87
|
+
}
|
|
88
|
+
return stamped;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
const repair = process.argv.includes('--repair');
|
|
93
|
+
const concurrencyArg = process.argv.indexOf('--concurrency');
|
|
94
|
+
const concurrency = Math.max(1, Number(concurrencyArg >= 0 ? process.argv[concurrencyArg + 1] : 3));
|
|
95
|
+
const namesAt = process.argv.indexOf('--names');
|
|
96
|
+
const names = namesAt >= 0
|
|
97
|
+
? new Set(String(process.argv[namesAt + 1] || '').split(',').filter(Boolean))
|
|
98
|
+
: null;
|
|
99
|
+
const kbDirAt = process.argv.indexOf('--dir');
|
|
100
|
+
const kbDir = path.resolve(ROOT, kbDirAt >= 0 ? process.argv[kbDirAt + 1] : 'kb');
|
|
101
|
+
const rvfPaths = fs.readdirSync(kbDir)
|
|
102
|
+
.filter((name) => name.endsWith('.rvf') && (!names || names.has(name.replace(/\.rvf$/, ''))))
|
|
103
|
+
.sort()
|
|
104
|
+
.map((name) => path.join(kbDir, name));
|
|
105
|
+
const { mod: { RvfDatabase } } = loadRvf();
|
|
106
|
+
const results = new Array(rvfPaths.length);
|
|
107
|
+
let next = 0;
|
|
108
|
+
|
|
109
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, rvfPaths.length || 1) }, async () => {
|
|
110
|
+
for (let index = next++; index < rvfPaths.length; index = next++) {
|
|
111
|
+
const rvfPath = rvfPaths[index];
|
|
112
|
+
try {
|
|
113
|
+
results[index] = repair
|
|
114
|
+
? await repairRvfIndex(rvfPath, RvfDatabase)
|
|
115
|
+
: await inspectRvfIndex(rvfPath, RvfDatabase);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
results[index] = { path: rvfPath, state: 'FAIL', error: error.message };
|
|
118
|
+
}
|
|
119
|
+
const row = results[index];
|
|
120
|
+
console.log(JSON.stringify({ ...row, path: path.relative(ROOT, row.path) }));
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
const failed = results.filter(({ state }) => state !== 'PASS');
|
|
125
|
+
const repaired = results.filter(({ repaired }) => repaired).length;
|
|
126
|
+
const stamped = repair && !failed.length
|
|
127
|
+
? restampChangedRvfGenerations(kbDir, results)
|
|
128
|
+
: 0;
|
|
129
|
+
console.log(`SUMMARY checked=${results.length} repaired=${repaired} stamped=${stamped} failed=${failed.length}`);
|
|
130
|
+
if (failed.length) process.exitCode = 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
134
|
+
main().catch((error) => {
|
|
135
|
+
console.error(error.stack || error.message);
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// self-update.mjs — nightly evergreen driver.
|
|
3
|
+
// Compares each in-scope repo's live HEAD against the stamped manifest, then (re)builds the segments
|
|
4
|
+
// that changed and re-stamps. Dry-run by default; pass --apply to actually (re)build.
|
|
5
|
+
//
|
|
6
|
+
// node scripts/self-update.mjs # dry-run: print the rebuild plan
|
|
7
|
+
// node scripts/self-update.mjs --apply # rebuild stale/pending repos (serial; embedding is CPU-bound)
|
|
8
|
+
// node scripts/self-update.mjs --apply --tier T0 # limit scope
|
|
9
|
+
// node scripts/self-update.mjs --apply --repo ruflo
|
|
10
|
+
//
|
|
11
|
+
// Publication is intentionally NOT supported here. A scheduled or local rebuild may prepare
|
|
12
|
+
// candidate bytes, but only the protected release workflow may publish them after release-proof
|
|
13
|
+
// seals the exact clean SHA and packed-artifact digest.
|
|
14
|
+
//
|
|
15
|
+
// Designed to be invoked by deploy/com.ruvnet.brain-nightly.plist (LaunchAgent — NOT auto-installed).
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { FULL_HINTS, KEEP_DIRS } from './full-hints.mjs';
|
|
22
|
+
import { withSubmoduleSymlinksDetached } from './git-clone-refresh.mjs';
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
|
|
26
|
+
// launchd delivers a minimal PATH, and the plist-level export has now failed across TWO separate
|
|
27
|
+
// reloads for TWO separate reasons: (2026-07-10) `gh release create` ENOENT, pid 0, while the
|
|
28
|
+
// loaded plist showed the export; (2026-07-12) launchd's WorkingDirectory was unset entirely so
|
|
29
|
+
// the job never even reached this file. Both are now fixed at the plist level too, but the script
|
|
30
|
+
// guarantees its own environment regardless: /usr/local/bin FIRST (that's the node every build
|
|
31
|
+
// tonight was tested and gated against — 22.13.1; homebrew independently upgraded to 25.9.0,
|
|
32
|
+
// which cannot load kb/'s native @ruvector/rvf module) and every spawned tool resolved to an
|
|
33
|
+
// absolute path, never bare-name PATH lookup — a bare `node`/`gh` silently picks up whichever
|
|
34
|
+
// version macOS or homebrew put first, and that has now broken this pipeline twice.
|
|
35
|
+
process.env.PATH = ['/usr/local/bin', '/opt/homebrew/bin', process.env.PATH || ''].join(':');
|
|
36
|
+
const GH = ['/usr/local/bin/gh', '/opt/homebrew/bin/gh'].find((p) => fs.existsSync(p)) || 'gh';
|
|
37
|
+
const NODE = ['/usr/local/bin/node', '/opt/homebrew/bin/node'].find((p) => fs.existsSync(p)) || process.execPath;
|
|
38
|
+
// Fourth instance of the same disease: kb/resolve-deps.mjs's loadRvf()/loadTransformers() fall
|
|
39
|
+
// back to NODE_PATH / bare require() resolution, which only works when the invoking shell's rc
|
|
40
|
+
// files happened to export it — true for an interactive login shell, silently false for launchd's
|
|
41
|
+
// bash -lc (non-interactive, so any `[[ $- != *i* ]] && return`-style guard in .bash_profile skips
|
|
42
|
+
// the rest of the file). Never depend on ambient shell state for a scheduled job again: pin both
|
|
43
|
+
// deps to their real, verified absolute locations via resolve-deps.mjs's own explicit overrides.
|
|
44
|
+
if (!process.env.RVF_MODULE_PATH && fs.existsSync('/Users/stuartkerr/.npm-global/lib/node_modules/@ruvector/rvf')) {
|
|
45
|
+
process.env.RVF_MODULE_PATH = '/Users/stuartkerr/.npm-global/lib/node_modules';
|
|
46
|
+
}
|
|
47
|
+
if (!process.env.XENOVA_PATH && fs.existsSync('/Users/stuartkerr/.npm-global/node_modules/@xenova/transformers')) {
|
|
48
|
+
process.env.XENOVA_PATH = '/Users/stuartkerr/.npm-global/node_modules/@xenova/transformers';
|
|
49
|
+
}
|
|
50
|
+
const has = (f) => process.argv.includes(f);
|
|
51
|
+
const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
|
|
52
|
+
const APPLY = has('--apply');
|
|
53
|
+
const TIER = arg('--tier', null);
|
|
54
|
+
const ONLY = arg('--repo', null);
|
|
55
|
+
if (has('--publish')) {
|
|
56
|
+
console.error('[release-authority] DENIED: self-update is rebuild-only; --publish cannot create a GitHub Release, publish npm, move a dist-tag, push a release commit, or label anything shipped.');
|
|
57
|
+
console.error('[release-authority] Use the protected release workflow after release-proof seals the exact clean SHA and packed-artifact digest.');
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
// --publish's whole job is to end this run with `git commit` + `git push origin main` (see the
|
|
61
|
+
// PUBLISH block far below) — and those two commands operate on whatever branch is CURRENTLY
|
|
62
|
+
// CHECKED OUT in this working tree, not necessarily main. Real failure (2026-07-19): the nightly
|
|
63
|
+
// fired at 03:15 while a developer had feat/meta-proxy-passthrough checked out, and commit 4a10833
|
|
64
|
+
// "Nightly brain refresh v3.4.21-dev" landed silently on THAT branch — a version-bump commit no
|
|
65
|
+
// release will ever be cut from, stranded on top of someone's unrelated in-progress work.
|
|
66
|
+
// Guard here, before ANY of the (hours-long, CPU-bound) rebuild work below even starts, and before
|
|
67
|
+
// the GitHub Release / npm publish steps run — checking branch only at commit-time would still
|
|
68
|
+
// burn a full night's rebuild, or worse, leave a Release cut with no matching version-bump commit
|
|
69
|
+
// to go with it. We deliberately do NOT `git checkout main` to self-correct: this machine is
|
|
70
|
+
// routinely mid-task on a feature branch at 03:15 (that is exactly how this bug was found), and
|
|
71
|
+
// switching branches out from under someone's uncommitted work is precisely the "clever and
|
|
72
|
+
// destructive" move this repo has already been burned by once (see the -readonly/timeout lesson in
|
|
73
|
+
// CLAUDE.md Rule 19). An aborted nightly that logs clearly and reruns tomorrow costs nothing; a
|
|
74
|
+
// checkout that clobbers a developer's working tree cannot be undone.
|
|
75
|
+
// ── THE BRANCH GUARD IS GONE, AND THAT IS THE FIX (2026-07-24) ──────────────────────────────────
|
|
76
|
+
//
|
|
77
|
+
// It used to FATAL here when ROOT was not on main. That guard was CORRECT for what it knew: these
|
|
78
|
+
// git commands act on whatever branch is checked out, and on 2026-07-19 that stranded commit 4a10833
|
|
79
|
+
// on feat/meta-proxy-passthrough. Refusing beat clobbering a developer's working tree.
|
|
80
|
+
//
|
|
81
|
+
// But refusing had a cost nobody was watching. This file is the ONLY thing that cuts GitHub Releases
|
|
82
|
+
// (`release.mjs --publish` does npm and contains zero `gh release` calls). So on 2026-07-23 the
|
|
83
|
+
// nightly hit this guard — the repo was on branch 4.0 — aborted correctly, and the release channel
|
|
84
|
+
// froze at v3.9.18-dev while npm advanced to 3.9.50. Thirty-two versions of drift produced by a guard
|
|
85
|
+
// working exactly as designed, invisible because the channel verifier only checked the bundle was
|
|
86
|
+
// REACHABLE, never that it was CURRENT.
|
|
87
|
+
//
|
|
88
|
+
// The dilemma was false. It only existed because the version commit ran in ROOT. It now runs in a
|
|
89
|
+
// worktree pinned to origin/main (see the PUBLISH block), so the developer's tree is never touched
|
|
90
|
+
// AND the release is never blocked by what they have checked out. Neither horn, rather than a better
|
|
91
|
+
// choice between them.
|
|
92
|
+
//
|
|
93
|
+
// Nothing replaces this check because nothing needs to: ROOT's branch is now irrelevant to publishing.
|
|
94
|
+
// If the worktree cannot be prepared, THAT fails loudly and refuses to fall back to committing in
|
|
95
|
+
// ROOT — the guarantee this guard existed to provide, kept, without the outage it caused.
|
|
96
|
+
// --fresh-window <days>: LIVE-scan the org(s) and take every non-fork/non-archived repo pushed within
|
|
97
|
+
// N days, bypassing the static registry.tiers.json AND the nightly T3 skip. This is THE fix for
|
|
98
|
+
// "new repos rUv shipped never got ingested" — a brand-new repo is discovered and built the same night
|
|
99
|
+
// it appears, but only within the rolling window, so the old "days of compute for all 173" risk that the
|
|
100
|
+
// tier gate guarded against still cannot happen. Absent this flag, scoping is byte-for-byte unchanged.
|
|
101
|
+
const FRESH_WINDOW = arg('--fresh-window', null);
|
|
102
|
+
const MODEL_CACHE = process.env.KB_MODEL_CACHE || path.join(ROOT, 'kb', 'models-cache');
|
|
103
|
+
|
|
104
|
+
// Optional author-side clone overrides via env (JSON map); default empty → uses CLONE_DIR/<name>.
|
|
105
|
+
const KNOWN_CLONES = JSON.parse(process.env.RUVNET_KNOWN_CLONES || '{}');
|
|
106
|
+
const CLONE_DIR = path.join(ROOT, 'clones');
|
|
107
|
+
// --full / --keep depth config per repo: SHARED map in scripts/full-hints.mjs (also used by
|
|
108
|
+
// ingest-repo.mjs, which previously had NO hints and silently zeroed full-body indexing on any
|
|
109
|
+
// rebuild routed through it). History + derivation rationale preserved in that file.
|
|
110
|
+
|
|
111
|
+
const tiers = JSON.parse(fs.readFileSync(path.join(ROOT, 'data/registry.tiers.json'), 'utf8'));
|
|
112
|
+
const manifest = fs.existsSync(path.join(ROOT, 'data/manifest.json'))
|
|
113
|
+
? JSON.parse(fs.readFileSync(path.join(ROOT, 'data/manifest.json'), 'utf8')) : { builtRepos: [] };
|
|
114
|
+
// case-insensitive: registry names are capitalized (RuVector) but built artifacts are lowercase (ruvector)
|
|
115
|
+
const builtSha = Object.fromEntries(manifest.builtRepos.map((r) => [r.name.toLowerCase(), r.builtFromSha]));
|
|
116
|
+
|
|
117
|
+
// owner/repo default to 'ruvnet'/<name> so existing registry entries (no owner/repo field) are
|
|
118
|
+
// unaffected; a repo living outside the ruvnet org (e.g. a different GitHub org) sets both.
|
|
119
|
+
const remoteHead = (owner, slug) => {
|
|
120
|
+
try { return execFileSync('git', ['ls-remote', `https://github.com/${owner}/${slug}`, 'HEAD'], { timeout: 30000 }).toString().split(/\s/)[0] || null; }
|
|
121
|
+
catch { return null; }
|
|
122
|
+
};
|
|
123
|
+
const clonePath = (name) => KNOWN_CLONES[name] || path.join(CLONE_DIR, name);
|
|
124
|
+
|
|
125
|
+
let inScope = [];
|
|
126
|
+
if (FRESH_WINDOW) {
|
|
127
|
+
const days = parseInt(FRESH_WINDOW, 10);
|
|
128
|
+
if (!Number.isFinite(days) || days <= 0) { console.error(`--fresh-window needs a positive day count, got "${FRESH_WINDOW}"`); process.exit(2); }
|
|
129
|
+
const cutoff = Date.now() - days * 86400 * 1000;
|
|
130
|
+
const orgs = (process.env.RUVNET_FRESH_ORGS || 'ruvnet').split(',').map((s) => s.trim()).filter(Boolean);
|
|
131
|
+
let orgFailures = 0;
|
|
132
|
+
for (const org of orgs) {
|
|
133
|
+
let rows;
|
|
134
|
+
try {
|
|
135
|
+
rows = JSON.parse(execFileSync(GH, ['repo', 'list', org, '--limit', '1000', '--json', 'name,pushedAt,isFork,isArchived'], { timeout: 60000 }).toString());
|
|
136
|
+
} catch (e) { orgFailures++; console.error(`[fresh-window] gh repo list ${org} FAILED: ${e.message} — skipping org (nothing from it this run)`); continue; }
|
|
137
|
+
for (const r of rows) {
|
|
138
|
+
if (r.isFork || r.isArchived) continue;
|
|
139
|
+
if (new Date(r.pushedAt).getTime() < cutoff) continue;
|
|
140
|
+
inScope.push({ name: r.name, owner: org, repo: r.name, tier: `${days}d`, pushedAt: r.pushedAt });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (ONLY) inScope = inScope.filter((r) => r.name === ONLY);
|
|
144
|
+
// FENCE: deliberately-removed (helix — a downstream consumer app) + empty repos must NOT be silently
|
|
145
|
+
// re-added by the rolling window. data/nightly-exclude.json is the one editable list; every drop logged.
|
|
146
|
+
const exPath = path.join(ROOT, 'data/nightly-exclude.json');
|
|
147
|
+
if (fs.existsSync(exPath)) {
|
|
148
|
+
try {
|
|
149
|
+
const exSet = new Set((JSON.parse(fs.readFileSync(exPath, 'utf8')).exclude || []).map((s) => String(s).toLowerCase()));
|
|
150
|
+
const dropped = inScope.filter((r) => exSet.has(r.name.toLowerCase())).map((r) => r.name);
|
|
151
|
+
inScope = inScope.filter((r) => !exSet.has(r.name.toLowerCase()));
|
|
152
|
+
if (dropped.length) console.log(`[fresh-window] fenced out ${dropped.length}: ${dropped.join(', ')} (data/nightly-exclude.json)`);
|
|
153
|
+
} catch (e) { console.error(`[fresh-window] nightly-exclude.json unreadable (${e.message}) — proceeding WITHOUT fence`); }
|
|
154
|
+
}
|
|
155
|
+
console.log(`[fresh-window] live scan of [${orgs.join(', ')}] → ${inScope.length} repos in scope after fence, pushed since ${new Date(cutoff).toISOString().slice(0, 10)} (${days}d window)`);
|
|
156
|
+
} else {
|
|
157
|
+
for (const t of ['T0', 'T1', 'T2', 'T3']) {
|
|
158
|
+
if (TIER && t !== TIER) continue;
|
|
159
|
+
for (const r of tiers.tiers[t].repos) {
|
|
160
|
+
if (ONLY && r.name !== ONLY) continue;
|
|
161
|
+
if (t === 'T3' && !ONLY) continue; // T3 is deep-walked on demand, not nightly
|
|
162
|
+
inScope.push({ ...r, tier: t });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const plan = [];
|
|
168
|
+
// DERIVED, not asserted (F4, 2026-07-18): remoteHead() returns null on ANY probe failure, and the
|
|
169
|
+
// old planner labeled a null-probe repo "up-to-date" — so a network-dark nightly (GitHub unreachable,
|
|
170
|
+
// DNS down, token dead) produced an empty todo, exit 0, and the wrapper logged "CLEAN NO-OP" while
|
|
171
|
+
// having measured NOTHING. That is the exact "blind tool reports health" bug stack-sync.mjs kills
|
|
172
|
+
// with its registry guard. Now: an individually-failed probe is labeled 'unverified (probe failed)' —
|
|
173
|
+
// never "up-to-date", never triggering a rebuild — and if HALF OR MORE of the probes failed the run
|
|
174
|
+
// REFUSES to claim anything (exit 1), letting the wrapper's retry + escalation do their job.
|
|
175
|
+
let probeFailures = 0;
|
|
176
|
+
for (const r of inScope) {
|
|
177
|
+
// `r.org` is read as well as `r.owner`, because the registry uses BOTH spellings and reading only
|
|
178
|
+
// one silently mis-resolves the other. MEASURED 2026-07-24: agentic-qe carries org:
|
|
179
|
+
// "proffesor-for-testing", the code read r.owner (undefined), fell through to the 'ruvnet' default,
|
|
180
|
+
// and probed github.com/ruvnet/agentic-qe — which 404s. The repo was reported "unverified (probe
|
|
181
|
+
// failed)" on every single run, which reads as "the remote is down" when the truth was that we
|
|
182
|
+
// asked about a repository that does not exist. Live check: ruvnet/agentic-qe -> 404,
|
|
183
|
+
// proffesor-for-testing/agentic-qe -> 200.
|
|
184
|
+
const live = remoteHead(r.owner || r.org || 'ruvnet', r.repo || r.name);
|
|
185
|
+
if (live === null) probeFailures++;
|
|
186
|
+
const built = builtSha[r.name.toLowerCase()] || null;
|
|
187
|
+
let action = 'up-to-date';
|
|
188
|
+
if (!built) action = 'build (new)';
|
|
189
|
+
// 'unknown' stamped SHA cannot prove freshness — treat as changed whenever upstream is reachable.
|
|
190
|
+
// (Real bug: ruflo + agentic-flow were stamped unknown and therefore NEVER auto-rebuilt — the two
|
|
191
|
+
// most-central repos were invisible to the freshness loop until a 3.24 release exposed it.)
|
|
192
|
+
else if (built === 'unknown') action = live ? 'rebuild (changed)' : 'unverified (probe failed)';
|
|
193
|
+
else if (live === null) action = 'unverified (probe failed)';
|
|
194
|
+
else if (live !== built) action = 'rebuild (changed)';
|
|
195
|
+
plan.push({ name: r.name, owner: r.owner, repo: r.repo, tier: r.tier, built: built?.slice(0, 12) || '—', live: live?.slice(0, 12) || '?', action });
|
|
196
|
+
}
|
|
197
|
+
if (inScope.length > 0 && probeFailures * 2 >= inScope.length) {
|
|
198
|
+
console.error(`\n[blind-guard] ${probeFailures}/${inScope.length} freshness probes FAILED — refusing to claim "up-to-date" for anything. This run measured nothing; that is a failure, not a no-op.`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
if (typeof orgFailures !== 'undefined' && orgFailures > 0 && inScope.length === 0) {
|
|
202
|
+
console.error(`\n[blind-guard] every org listing failed (${orgFailures}) and nothing is in scope — cannot distinguish "nothing fresh" from "completely blind". Failing loudly.`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`self-update ${APPLY ? '(APPLY)' : '(dry-run)'} — ${plan.length} repos in scope\n`);
|
|
207
|
+
for (const p of plan) console.log(` ${p.action.padEnd(20)} ${p.tier} ${p.name.padEnd(24)} built:${p.built} live:${p.live}`);
|
|
208
|
+
// SAFE NIGHTLY SCOPE: by default only REBUILD already-built repos whose upstream changed (keeps the
|
|
209
|
+
// shipped bundle current). Building brand-new repos is a supervised, multi-hour scaling effort, NOT an
|
|
210
|
+
// unattended nightly job — gate it behind --include-new so the cron can't silently try to deep-walk 40+
|
|
211
|
+
// repos (days of compute) on its first run.
|
|
212
|
+
// Fresh-window mode exists precisely to discover + build brand-new repos, so default INCLUDE_NEW on
|
|
213
|
+
// there (still bounded to the N-day set). Static-tier mode keeps the original opt-in gate untouched.
|
|
214
|
+
const INCLUDE_NEW = has('--include-new') || !!FRESH_WINDOW;
|
|
215
|
+
const changed = plan.filter((p) => p.action === 'rebuild (changed)');
|
|
216
|
+
const newRepos = plan.filter((p) => p.action === 'build (new)');
|
|
217
|
+
const todo = INCLUDE_NEW ? [...changed, ...newRepos] : changed;
|
|
218
|
+
console.log(`\nrebuild(changed): ${changed.map((p) => p.name).join(', ') || 'none'}`);
|
|
219
|
+
console.log(`new(not built): ${newRepos.length} repos${INCLUDE_NEW ? ' — INCLUDED (--include-new)' : ' — SKIPPED (supervised; pass --include-new to build)'}`);
|
|
220
|
+
console.log(`→ this run will (re)build ${todo.length}: ${todo.map((p) => p.name).join(', ') || 'none'}`);
|
|
221
|
+
|
|
222
|
+
if (!APPLY) {
|
|
223
|
+
console.log('\n(dry-run — pass --apply to (re)build; runs serially since embedding is CPU-bound)');
|
|
224
|
+
// Sol amendment to F4: even a dry-run may not report clean (exit 0) when probes failed — the
|
|
225
|
+
// "CLEAN NO-OP" the wrapper logs on exit 0 must mean "measured everything, nothing due".
|
|
226
|
+
if (probeFailures > 0) { console.error(`[blind-guard] ${probeFailures} probe(s) failed — dry-run cannot certify freshness; exiting 1`); process.exit(1); }
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const NOTIFY = (t, m, p) => { try { execFileSync('sh', [path.join(ROOT, 'scripts/notify.sh'), t, m, p || 'default']); } catch { /* alerting never breaks the pipeline */ } };
|
|
231
|
+
const failures = []; // per-repo build failures collected here; ANY failure aborts before publish (see below)
|
|
232
|
+
for (const p of todo) {
|
|
233
|
+
const dir = clonePath(p.name);
|
|
234
|
+
try {
|
|
235
|
+
if (!fs.existsSync(path.join(dir, '.git'))) {
|
|
236
|
+
console.log(`[clone] ${p.name}`);
|
|
237
|
+
fs.mkdirSync(CLONE_DIR, { recursive: true });
|
|
238
|
+
execFileSync('git', ['clone', '--depth', '1', `https://github.com/${p.owner || p.org || 'ruvnet'}/${p.repo || p.name}`, dir], { stdio: 'inherit' });
|
|
239
|
+
} else {
|
|
240
|
+
// Some cached clones deduplicate large submodules with symlinks to another clone. Git refuses
|
|
241
|
+
// even a fetch when a gitlink path is a symlink ("expected submodule path ... not to be a
|
|
242
|
+
// symbolic link"). Detach only those declared submodule symlinks for fetch/reset, then restore
|
|
243
|
+
// them in finally so a network/reset failure cannot strand the cache in a half-repaired state.
|
|
244
|
+
withSubmoduleSymlinksDetached(dir, () => {
|
|
245
|
+
execFileSync('git', ['-C', dir, 'fetch', '--depth', '1', 'origin'], { stdio: 'inherit' });
|
|
246
|
+
execFileSync('git', ['-C', dir, 'reset', '--hard', 'origin/HEAD'], { stdio: 'inherit' });
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const env = { ...process.env, KB_MODEL_CACHE: MODEL_CACHE };
|
|
250
|
+
const kb = p.name.toLowerCase(); // artifacts are lowercase (ruvector.rvf), registry name is RuVector
|
|
251
|
+
const full = FULL_HINTS[kb];
|
|
252
|
+
const keep = KEEP_DIRS[kb];
|
|
253
|
+
const buildArgs = ['forge-refresh.mjs', '--repo', dir, '--out', '.', '--name', kb,
|
|
254
|
+
'--canonical-url', process.env.RUVNET_CANONICAL_URL || 'https://raw.githubusercontent.com/stuinfla/ruvnet-brain/main/kb'];
|
|
255
|
+
if (full) buildArgs.push('--full', full);
|
|
256
|
+
if (keep) buildArgs.push('--keep', keep);
|
|
257
|
+
console.log(`[refresh] ${kb}`);
|
|
258
|
+
execFileSync(NODE, buildArgs, { cwd: path.join(ROOT, 'kb'), env, stdio: 'inherit' });
|
|
259
|
+
console.log(`[symbols] ${kb}`);
|
|
260
|
+
execFileSync(NODE, ['scripts/build-symbols.mjs', '--name', kb], { cwd: ROOT, env, stdio: 'inherit' });
|
|
261
|
+
// Corpus QA gate (scripts/corpus-qa.mjs): structural (passages>0, full-bodies>0 where
|
|
262
|
+
// FULL_HINTS demands them, vectors==passages, embed.json present) + deterministic
|
|
263
|
+
// self-retrieval round trip on every canonical store. Non-zero exit throws -> failures[] ->
|
|
264
|
+
// the run aborts before stamp/bundle/publish. This is the machine version of the
|
|
265
|
+
// 2026-07-10 hand-verification: a store that embeds wrong or reads wrong cannot ship.
|
|
266
|
+
console.log(`[qa] ${kb}`);
|
|
267
|
+
execFileSync(NODE, ['scripts/corpus-qa.mjs', '--store', kb], { cwd: ROOT, env, stdio: 'inherit' });
|
|
268
|
+
} catch (e) { console.error(`[FAIL] ${p.name}: ${e.message}`); failures.push({ name: p.name, error: e.message }); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A per-repo build failure used to be logged and swallowed before the run re-stamped and re-bundled
|
|
272
|
+
// partial data. Fail loud: if ANY repo failed, abort before stamp/bundle. This rebuild process has no
|
|
273
|
+
// publication authority; a later protected release may consume only a fully prepared candidate.
|
|
274
|
+
if (failures.length) {
|
|
275
|
+
NOTIFY('🔴 Nightly brain rebuild ABORTED', `${failures.length} repo build(s) failed — no candidate prepared. See logs/nightly.log`, 'urgent');
|
|
276
|
+
console.error(`\n[FATAL] ${failures.length} repo build(s) failed this run — aborting before stamp/bundle. No candidate prepared:`);
|
|
277
|
+
for (const f of failures) console.error(` - ${f.name}: ${f.error}`);
|
|
278
|
+
console.error('Fix the failing repo(s) and re-run.');
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log('\n[stamp] re-stamping bundle');
|
|
283
|
+
execFileSync(NODE, ['scripts/brain-stamp.mjs'], { cwd: ROOT, stdio: 'inherit' });
|
|
284
|
+
// refresh the shipped bundle so the rebuilt deep-source ships (concepts/primers/L2 are supervised — not regenerated here)
|
|
285
|
+
console.log('[bundle] re-assembling dist/ruvnet-brain');
|
|
286
|
+
execFileSync(NODE, ['scripts/build-bundle.mjs'], { cwd: ROOT, env: { ...process.env, KB_MODEL_CACHE: MODEL_CACHE }, stdio: 'inherit' });
|
|
287
|
+
console.log('self-update done. (Deep-source refreshed + bundle re-assembled. Primer/L2/concepts + grading are supervised steps — re-run them when a repo materially changes.)');
|
|
288
|
+
|
|
289
|
+
// Sol amendment to F4 (end of run): the verified rebuilds above are real work and were NOT
|
|
290
|
+
// skipped — but if any freshness probe failed, this run still may not report clean. Exit 1 AFTER the
|
|
291
|
+
// work so the wrapper's retry re-probes (rebuilds are change-gated, so the retry is cheap) and a
|
|
292
|
+
// persistent blind spot escalates instead of hiding inside a green "no-op".
|
|
293
|
+
if (typeof probeFailures !== 'undefined' && probeFailures > 0) {
|
|
294
|
+
console.error(`\n[blind-guard] run completed its verified work, but ${probeFailures} freshness probe(s) FAILED — those repos are 'unverified', not 'up-to-date'. Exiting 1 so the failure is visible.`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
package/scripts/selfcheck.mjs
CHANGED
|
@@ -424,7 +424,13 @@ export function assertContract({ rec, measurement, mode, timeoutSec }) {
|
|
|
424
424
|
if (measurement.timedOut) {
|
|
425
425
|
v.push({ kind: 'hang', where, regime: measurement.regime, detail: `${tag} did not exit within its declared timeout of ${timeoutSec}s — the watchdog had to kill the process group` });
|
|
426
426
|
} else if (measurement.elapsedMs > budgetMs * TIMEOUT_MARGIN) {
|
|
427
|
-
|
|
427
|
+
const stageTrace = String(measurement.stderr || '')
|
|
428
|
+
.split('\n')
|
|
429
|
+
.filter((line) => line.includes('SESSION_TRACE'))
|
|
430
|
+
.map((line) => line.trim())
|
|
431
|
+
.join(' | ');
|
|
432
|
+
const traceDetail = stageTrace ? `; stage trace: ${stageTrace}` : '';
|
|
433
|
+
v.push({ kind: 'slow', where, regime: measurement.regime, detail: `${tag} took ${measurement.elapsedMs}ms — over ${Math.round(TIMEOUT_MARGIN * 100)}% of its ${timeoutSec}s timeout (no margin left)${traceDetail}` });
|
|
428
434
|
}
|
|
429
435
|
|
|
430
436
|
// 2. EXIT CODE within the declared contract for this mode.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/sign-bundle.mjs — Ed25519 signing for the shipped brain bundle (SEC-0010 #6).
|
|
3
|
+
//
|
|
4
|
+
// WHY: the installer downloads a ~512 MB zip over the network and extracts it into the user's
|
|
5
|
+
// Claude Code config. Unsigned, a MITM or a compromised release host could swap the bundle for a
|
|
6
|
+
// poisoned one and the installer would extract it blindly. Signing closes that: publish emits a
|
|
7
|
+
// detached signature over the bundle's SHA-256; the installer verifies it against a PUBLIC key that
|
|
8
|
+
// ships INSIDE the npm package (so the trust root travels with the installer, not the download).
|
|
9
|
+
//
|
|
10
|
+
// Design rules (match the rest of the repo): dependency-free (node:crypto only), and the private key
|
|
11
|
+
// NEVER lives in the repo — it's read from $RUVNET_SIGNING_KEY (PEM) or a gitignored key file.
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// node scripts/sign-bundle.mjs --gen-key # one-time: make a keypair (pub → repo, priv → gitignored)
|
|
15
|
+
// node scripts/sign-bundle.mjs [--bundle <zip>] # sign the bundle → <zip>.sha256 + <zip>.sig
|
|
16
|
+
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
|
|
24
|
+
const has = (f) => process.argv.includes(f);
|
|
25
|
+
|
|
26
|
+
// The PUBLIC key ships in the package (committed) — it is the installer's trust root.
|
|
27
|
+
const PUB_PATH = path.join(ROOT, 'keys', 'ruvnet-brain-signing.pub.pem');
|
|
28
|
+
// The PRIVATE key is a secret: env first, then a gitignored file. NEVER committed.
|
|
29
|
+
const PRIV_ENV = 'RUVNET_SIGNING_KEY';
|
|
30
|
+
const PRIV_FILE = process.env.RUVNET_SIGNING_KEY_FILE || path.join(ROOT, '.secrets', 'ruvnet-brain-signing.key.pem');
|
|
31
|
+
|
|
32
|
+
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
|
|
33
|
+
|
|
34
|
+
function genKey() {
|
|
35
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
36
|
+
const pub = publicKey.export({ type: 'spki', format: 'pem' });
|
|
37
|
+
const priv = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
38
|
+
fs.mkdirSync(path.dirname(PUB_PATH), { recursive: true });
|
|
39
|
+
fs.mkdirSync(path.dirname(PRIV_FILE), { recursive: true });
|
|
40
|
+
fs.writeFileSync(PUB_PATH, pub);
|
|
41
|
+
fs.writeFileSync(PRIV_FILE, priv, { mode: 0o600 });
|
|
42
|
+
console.log(`✓ public key → ${path.relative(ROOT, PUB_PATH)} (COMMIT this — it's the installer's trust root)`);
|
|
43
|
+
console.log(`✓ private key → ${path.relative(ROOT, PRIV_FILE)} (gitignored; keep secret, or set $${PRIV_ENV})`);
|
|
44
|
+
console.log(` Rotate by re-running --gen-key and re-signing; the old public key stops verifying.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadPrivateKey() {
|
|
48
|
+
if (process.env[PRIV_ENV]) return crypto.createPrivateKey(process.env[PRIV_ENV]);
|
|
49
|
+
if (fs.existsSync(PRIV_FILE)) return crypto.createPrivateKey(fs.readFileSync(PRIV_FILE, 'utf8'));
|
|
50
|
+
console.error(`✗ no signing key: set $${PRIV_ENV} (PEM) or create ${path.relative(ROOT, PRIV_FILE)} (run --gen-key).`);
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function signBundle() {
|
|
55
|
+
const bundle = arg('--bundle', path.join(ROOT, 'dist', 'ruvnet-brain.zip'));
|
|
56
|
+
if (!fs.existsSync(bundle)) { console.error(`✗ bundle not found: ${bundle}`); process.exit(2); }
|
|
57
|
+
const buf = fs.readFileSync(bundle);
|
|
58
|
+
const digest = sha256(buf);
|
|
59
|
+
fs.writeFileSync(`${bundle}.sha256`, `${digest} ${path.basename(bundle)}\n`);
|
|
60
|
+
// Sign the RAW digest bytes (Ed25519 takes the message directly; no pre-hash algo arg).
|
|
61
|
+
const sig = crypto.sign(null, Buffer.from(digest, 'hex'), loadPrivateKey());
|
|
62
|
+
fs.writeFileSync(`${bundle}.sig`, sig);
|
|
63
|
+
console.log(`✓ signed ${path.basename(bundle)}`);
|
|
64
|
+
console.log(` sha256: ${digest}`);
|
|
65
|
+
console.log(` → ${path.basename(bundle)}.sha256 + ${path.basename(bundle)}.sig (publish these alongside the zip)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (has('--gen-key')) genKey();
|
|
69
|
+
else signBundle();
|