ruvnet-brain 4.4.1 → 4.5.0
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 +3 -3
- package/bin/install.mjs +612 -113
- package/console/app.js +178 -81
- package/console/index.html +1 -1
- package/console/install-architecture.html +1 -0
- package/console/scope.css +4 -1
- package/console/style.css +13 -0
- package/console/tips.html +4 -4
- package/kb/brain-profile.mjs +1 -0
- package/kb/corpus-release-identity.mjs +1 -1
- package/kb/forge-update.mjs +41 -17
- package/kb/model-requirements.mjs +4 -1
- package/kb/update-storage-transaction.mjs +79 -0
- package/kb/zip-extract.mjs +22 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +5 -4
- package/plugin/commands/configure.md +5 -4
- package/plugin/commands/rnb-brief.md +41 -0
- package/plugin/commands/rnb.md +80 -0
- package/plugin/commands/rnbc.md +80 -0
- package/plugin/commands/rvbc.md +5 -4
- package/plugin/commands/rvcb.md +5 -4
- package/plugin/commands/whats-new.md +4 -4
- package/plugin/mcp/server.mjs +10 -2
- package/plugin/scripts/advocacy-route.mjs +59 -23
- package/plugin/scripts/anticipate.sh +4 -0
- package/plugin/scripts/brain-confirmation.mjs +258 -0
- package/plugin/scripts/brain-footprint.mjs +494 -0
- package/plugin/scripts/brain-location.mjs +47 -0
- package/plugin/scripts/capability-registry.mjs +11 -1
- package/plugin/scripts/continuity-brief.mjs +324 -0
- package/plugin/scripts/continuity-events.mjs +327 -0
- package/plugin/scripts/continuity-journal.mjs +500 -0
- package/plugin/scripts/decision-gate.mjs +56 -3
- package/plugin/scripts/footprint-io.mjs +186 -0
- package/plugin/scripts/ground-before-write.sh +8 -1
- package/plugin/scripts/ground-ruvnet.sh +103 -12
- package/plugin/scripts/grounding-answer.mjs +2 -1
- package/plugin/scripts/grounding-stamp.sh +3 -0
- package/plugin/scripts/grounding-substance.mjs +1 -1
- package/plugin/scripts/grounding-turn-evidence.mjs +17 -2
- package/plugin/scripts/hook-input.mjs +78 -4
- package/plugin/scripts/kb-copy-proof.mjs +148 -0
- package/plugin/scripts/lesson-bridge.mjs +6 -2
- package/plugin/scripts/nightly-controller.mjs +8 -1
- package/plugin/scripts/node-sqlite.mjs +41 -0
- package/plugin/scripts/package-cards.json +797 -0
- package/plugin/scripts/package-cards.rvf +0 -0
- package/plugin/scripts/package-cards.rvf.idmap.json +1 -0
- package/plugin/scripts/package-cards.rvf.meta.json +1 -0
- package/plugin/scripts/package-recommender-client.mjs +138 -0
- package/plugin/scripts/package-recommender-flag.mjs +30 -0
- package/plugin/scripts/package-recommender.mjs +391 -0
- package/plugin/scripts/project-progression-outbox.mjs +26 -8
- package/plugin/scripts/project-progression-reader.mjs +4 -2
- package/plugin/scripts/protect-brain-state.sh +4 -1
- package/plugin/scripts/session-snapshot-hook.mjs +27 -3
- package/plugin/scripts/session-start-budget.mjs +1 -0
- package/plugin/scripts/session-start-core.mjs +34 -4
- package/plugin/scripts/session-start-health.mjs +7 -1
- package/plugin/scripts/session-start-update-plane.mjs +35 -0
- package/plugin/scripts/turn-outcome-capture.mjs +12 -1
- package/plugin/scripts/unprompted-runtime.mjs +2 -2
- package/plugin/skills/brain-console/SKILL.md +3 -3
- package/plugin/skills/rnbc/SKILL.md +24 -0
- package/plugin/skills/rvbc/SKILL.md +2 -2
- package/scripts/approved-runtime.mjs +2 -2
- package/scripts/ci/warm-brain-models.mjs +28 -0
- package/scripts/codex-hook-trust.mjs +94 -0
- package/scripts/console-runtime-identity.mjs +5 -0
- package/scripts/corpus-canary.mjs +46 -6
- package/scripts/corpus-dispatch-decision.mjs +2 -2
- package/scripts/corpus-promotion.mjs +1 -1
- package/scripts/hook-qualify-hosts.mjs +15 -3
- package/scripts/host-install-matrix.mjs +63 -2
- package/scripts/human-approval-phrases.mjs +46 -0
- package/scripts/installed-brain-health.mjs +53 -0
- package/scripts/move-brain.mjs +310 -0
- package/scripts/onboarding-console.mjs +93 -10
- package/scripts/oracle/abstain-threshold-sweep.mjs +62 -0
- package/scripts/oracle/abstain-trace.mjs +139 -0
- package/scripts/oracle/doc2query-generate.mjs +162 -0
- package/scripts/oracle/doc2query-reach.mjs +110 -0
- package/scripts/oracle/judge-train.mjs +158 -0
- package/scripts/oracle/need-set-split.mjs +48 -0
- package/scripts/oracle/sona-query-adapter-eval.mjs +139 -0
- package/scripts/package-cards.mjs +374 -0
- package/scripts/publication-receipt.mjs +37 -9
- package/scripts/recommendation-e2e.mjs +110 -0
- package/scripts/recommendation-eval.mjs +105 -0
- package/scripts/recommendation-floor.mjs +56 -0
- package/scripts/recommendation-judge-score.mjs +74 -0
- package/scripts/recommendation-latency.mjs +95 -0
- package/scripts/recommendation-real-host-score.mjs +76 -0
- package/scripts/recommendation-real-host.mjs +137 -0
- package/scripts/release-channel-kind.mjs +1 -1
- package/scripts/release-environment-policy.mjs +33 -0
- package/scripts/single-source-check.mjs +15 -10
- package/scripts/sync-commands.mjs +5 -2
- package/scripts/wired-check.mjs +17 -2
package/kb/brain-profile.mjs
CHANGED
|
@@ -31,6 +31,7 @@ export function discoverStoreFamilies(dir) {
|
|
|
31
31
|
let entries = [];
|
|
32
32
|
try { entries = fs.readdirSync(dir); } catch { return []; }
|
|
33
33
|
for (const entry of entries) {
|
|
34
|
+
if (entry.startsWith('._')) continue; // macOS AppleDouble from an exFAT/FAT volume, never a store
|
|
34
35
|
const match = entry.match(/^(.+?)(?:\.big)?\.rvf$/);
|
|
35
36
|
if (match && !/\.(?:idmap|embed)\b/.test(entry)) names.add(match[1]);
|
|
36
37
|
}
|
|
@@ -58,7 +58,7 @@ export const CORPUS_TAG_PATTERN = /^corpus-sha256-[0-9a-f]{64}$/;
|
|
|
58
58
|
// CORPUS_TAG_PATTERN above is already duplicated this same way for the same reason. Keep both copies
|
|
59
59
|
// byte-identical in behavior; tests/unit/corpus-release-identity-channel-parity.test.mjs pins that.
|
|
60
60
|
|
|
61
|
-
/**
|
|
61
|
+
/** Product (code) release: a plain semver tag. */
|
|
62
62
|
export const CODE_TAG_PATTERN = /^v\d+\.\d+\.\d+$/;
|
|
63
63
|
|
|
64
64
|
export function releaseKind(tag) {
|
package/kb/forge-update.mjs
CHANGED
|
@@ -25,10 +25,10 @@ import os from 'node:os';
|
|
|
25
25
|
import { execFileSync } from 'node:child_process';
|
|
26
26
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
27
|
import { createHash, createPublicKey, verify as verifySignature } from 'node:crypto';
|
|
28
|
-
import { extractZip } from './zip-extract.mjs';
|
|
28
|
+
import { extractZip, zipDeclaredBytes } from './zip-extract.mjs';
|
|
29
29
|
import { applyBrainProfile, discoverStoreFamilies, readBrainProfile } from './brain-profile.mjs';
|
|
30
30
|
import { acquireRefreshLock, releaseRefreshLock } from './refresh-run.mjs';
|
|
31
|
-
import { runStorageTransaction, treeIdentity, managedStorageInventory, storageDelta } from './update-storage-transaction.mjs';
|
|
31
|
+
import { runStorageTransaction, treeIdentity, managedStorageInventory, storageDelta, checkDiskSpace, directoryBytes } from './update-storage-transaction.mjs';
|
|
32
32
|
import { pruneLifecycleEvidence } from './lifecycle-evidence-retention.mjs';
|
|
33
33
|
import {
|
|
34
34
|
isCorpusReleaseTag, assertCorpusReleaseCompatible, readInstalledRuntime,
|
|
@@ -1003,7 +1003,10 @@ function storeInventory(dir) {
|
|
|
1003
1003
|
}
|
|
1004
1004
|
try {
|
|
1005
1005
|
// Inventory only: tolerate ordinary tooling symlinks (npm .bin). A symlinked .rvf still throws.
|
|
1006
|
-
|
|
1006
|
+
// macOS volume metadata (AppleDouble `._x.rvf`, .DS_Store) from an exFAT/FAT round trip is neither a
|
|
1007
|
+
// store nor an unclassified file: counting it kept every such backup as "holding a unique store".
|
|
1008
|
+
const files = relativeFiles(dir, '', { strict: false })
|
|
1009
|
+
.filter((name) => !/^(?:\._|\.DS_Store$)/.test(path.basename(name)));
|
|
1007
1010
|
for (const relative of files.filter((name) => name.endsWith('.rvf'))) {
|
|
1008
1011
|
if (!declaredFiles.has(relative)) {
|
|
1009
1012
|
stores.set(path.normalize(relative), relative);
|
|
@@ -1059,7 +1062,12 @@ function assertRedundantBackup(backup, live) {
|
|
|
1059
1062
|
throw new Error(`unclassified or different symbolic link: ${prior}`);
|
|
1060
1063
|
}
|
|
1061
1064
|
} else if (a.isDirectory() && b.isDirectory()) {
|
|
1062
|
-
|
|
1065
|
+
// macOS volume metadata (AppleDouble `._x` = x's extended attributes, .DS_Store = Finder view state)
|
|
1066
|
+
// written by an exFAT/FAT round trip is not Brain data, so it is never what makes a copy unique.
|
|
1067
|
+
for (const name of fs.readdirSync(prior)) {
|
|
1068
|
+
if (/^(?:\._|\.DS_Store$)/.test(name)) continue;
|
|
1069
|
+
compare(path.join(prior, name), path.join(current, name));
|
|
1070
|
+
}
|
|
1063
1071
|
} else if (a.isFile() && b.isFile()) {
|
|
1064
1072
|
if (a.size !== b.size || sha256File(prior) !== sha256File(current)) {
|
|
1065
1073
|
throw new Error(`different bytes: ${prior}`);
|
|
@@ -1107,19 +1115,6 @@ function writeSnapshotReceipt(backupPath, { state, reason = null, recoveryComman
|
|
|
1107
1115
|
return receipt;
|
|
1108
1116
|
}
|
|
1109
1117
|
|
|
1110
|
-
/**
|
|
1111
|
-
* Release rollback copies after the new KB has verified (issue #35, Dr. Mark Allen).
|
|
1112
|
-
*
|
|
1113
|
-
* Exported and pure-ish because it DELETES MULTI-GIGABYTE DIRECTORIES — a bug here destroys user
|
|
1114
|
-
* data, so it is tested directly rather than exercised only through a full update run.
|
|
1115
|
-
*
|
|
1116
|
-
* Refuses to delete any backup holding a `.rvf` store the live KB does not have. That is the
|
|
1117
|
-
* private/local-store case: the public bundle does not ship those, the update replaces the directory,
|
|
1118
|
-
* and forge-guard still passes because it verifies the store it was asked about — not what went
|
|
1119
|
-
* missing. In that situation the backup is the only surviving copy, so it is kept and reported.
|
|
1120
|
-
*
|
|
1121
|
-
* @returns {{removed: string[], kept: [string, string][], freed: number}}
|
|
1122
|
-
*/
|
|
1123
1118
|
/**
|
|
1124
1119
|
* WHY a guard run failed. forge-guard prints its `[FAIL] ...` lines to STDOUT, and execFileSync puts only
|
|
1125
1120
|
* STDERR in error.message, so a refused store used to read "Command failed: node .../forge-guard.mjs --name X"
|
|
@@ -1137,6 +1132,19 @@ export function describeGuardFailure(error) {
|
|
|
1137
1132
|
return `${message.slice(0, 1600)}${cause}`;
|
|
1138
1133
|
}
|
|
1139
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* Release rollback copies after the new KB has verified (issue #35, Dr. Mark Allen).
|
|
1137
|
+
*
|
|
1138
|
+
* Exported and pure-ish because it DELETES MULTI-GIGABYTE DIRECTORIES — a bug here destroys user
|
|
1139
|
+
* data, so it is tested directly rather than exercised only through a full update run.
|
|
1140
|
+
*
|
|
1141
|
+
* Refuses to delete any backup holding a `.rvf` store the live KB does not have. That is the
|
|
1142
|
+
* private/local-store case: the public bundle does not ship those, the update replaces the directory,
|
|
1143
|
+
* and forge-guard still passes because it verifies the store it was asked about — not what went
|
|
1144
|
+
* missing. In that situation the backup is the only surviving copy, so it is kept and reported.
|
|
1145
|
+
*
|
|
1146
|
+
* @returns {{removed: string[], kept: [string, string][], freed: number}}
|
|
1147
|
+
*/
|
|
1140
1148
|
export function reclaimBackups({
|
|
1141
1149
|
kbDir,
|
|
1142
1150
|
backupsMade = [],
|
|
@@ -1710,6 +1718,22 @@ async function main() {
|
|
|
1710
1718
|
const signature = verifyDownloadedBundle(zipPath, sigPath);
|
|
1711
1719
|
if (!signature.ok) { fs.rmSync(tmp, { recursive: true, force: true }); die(`✗ SIGNATURE VERIFICATION FAILED: ${signature.reason}`, 4); }
|
|
1712
1720
|
console.log(` ✓ signature verified — ${signature.reason}`);
|
|
1721
|
+
// DISK-SPACE PREFLIGHT, before a single byte is unpacked: the bundle unpacks in temp, then a whole
|
|
1722
|
+
// candidate generation is built beside the live one: the bundle, plus what prepareCandidate carries into
|
|
1723
|
+
// it from live — node_modules, and every private/local-ingest store file (restorePrivateFilesIntoCandidate
|
|
1724
|
+
// copies them, so a brain with a large private store needs that much more; review S6). The downloaded
|
|
1725
|
+
// zip itself is already on disk at this point, so free space already reflects it.
|
|
1726
|
+
let space;
|
|
1727
|
+
try {
|
|
1728
|
+
const unpacked = zipDeclaredBytes(zipPath);
|
|
1729
|
+
const privateBytes = Object.values(privateOverlay?.files || {}).reduce((sum, file) => sum + (Number(file?.bytes) || 0), 0);
|
|
1730
|
+
space = checkDiskSpace([
|
|
1731
|
+
{ dir: extractDir, bytes: unpacked, purpose: 'unpacked bundle', brain: false },
|
|
1732
|
+
{ dir: path.dirname(KB_DIR), bytes: unpacked + directoryBytes(path.join(KB_DIR, 'node_modules')), purpose: 'new generation' },
|
|
1733
|
+
...(privateBytes ? [{ dir: path.dirname(KB_DIR), bytes: privateBytes, purpose: 'private stores carried into it' }] : []),
|
|
1734
|
+
]);
|
|
1735
|
+
} catch (error) { space = { ok: true, skipped: error.message }; } // unmeasurable: extraction's own limits still apply
|
|
1736
|
+
if (!space.ok) { fs.rmSync(tmp, { recursive: true, force: true }); die(space.message, 6); }
|
|
1713
1737
|
try { await extractZip(zipPath, extractDir); }
|
|
1714
1738
|
catch (error) { fs.rmSync(tmp, { recursive: true, force: true }); die(`extraction failed: ${error.message} — local files untouched.`); }
|
|
1715
1739
|
// ── TIER 3: the staged bundle names the runtime that built it; this client names the runtime it
|
|
@@ -3,6 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
export const MINILM_MODEL = 'Xenova/all-MiniLM-L6-v2';
|
|
5
5
|
export const BGE_MODEL = 'Xenova/bge-base-en-v1.5';
|
|
6
|
+
// The default cross-encoder every reranked answer loads (forge-rerank.mjs). Named here so the
|
|
7
|
+
// doctor can tell a cold cache from a broken reader without importing the ONNX stack.
|
|
8
|
+
export const RERANKER_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2';
|
|
6
9
|
|
|
7
10
|
export function modelPath(modelCache, model) {
|
|
8
11
|
return path.join(modelCache, ...model.split('/'));
|
|
@@ -71,7 +74,7 @@ export function requiredEmbedderModels(kbDir) {
|
|
|
71
74
|
const models = new Set();
|
|
72
75
|
try {
|
|
73
76
|
for (const file of fs.readdirSync(kbDir)) {
|
|
74
|
-
if (!file.endsWith('.rvf.embed.json')) continue;
|
|
77
|
+
if (!file.endsWith('.rvf.embed.json') || file.startsWith('._')) continue; // ._: AppleDouble, not a sidecar
|
|
75
78
|
const rvf = file.slice(0, -'.embed.json'.length);
|
|
76
79
|
if (!fs.existsSync(path.join(kbDir, rvf))) continue;
|
|
77
80
|
if (!rvf.endsWith('.big.rvf')) {
|
|
@@ -17,6 +17,85 @@ const STATES = Object.freeze([
|
|
|
17
17
|
'RECOVERY_REQUIRED',
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
+
// ── DISK-SPACE PREFLIGHT ──────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// An apply needs room for the unpacked bundle (temp), the candidate generation beside the live one
|
|
22
|
+
// (the bundle plus the live node_modules carried into it) and its receipts; measured ~3.3 GB growth and
|
|
23
|
+
// ~5 GB peak per apply on a 1.3 GB brain. Running out half-way leaves a half-built candidate and a
|
|
24
|
+
// confusing ENOSPC, so every install/update measures first and refuses cleanly — nothing touched.
|
|
25
|
+
export const DISK_HEADROOM_BYTES = 256 * 1024 ** 2;
|
|
26
|
+
|
|
27
|
+
function nearestExisting(dir) {
|
|
28
|
+
let current = path.resolve(dir);
|
|
29
|
+
while (!fs.existsSync(current)) {
|
|
30
|
+
const parent = path.dirname(current);
|
|
31
|
+
if (parent === current) break;
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Free bytes available to this user on the filesystem holding `dir` (or its nearest existing parent). */
|
|
38
|
+
export function availableBytes(dir, env = process.env, statfs = fs.statfsSync) {
|
|
39
|
+
// Test seam, honoured only under RUVNET_BRAIN_TEST=1: a full disk cannot be produced on demand.
|
|
40
|
+
if (env.RUVNET_BRAIN_TEST === '1' && /^\d+$/.test(String(env.RUVNET_TEST_FREE_BYTES || ''))) return Number(env.RUVNET_TEST_FREE_BYTES);
|
|
41
|
+
// fs.statfsSync arrived in Node 18.15; the package supports node >= 18. Say so plainly instead of a TypeError.
|
|
42
|
+
if (typeof statfs !== 'function') throw new Error(`this Node (${process.version}) cannot measure free disk space (no fs.statfsSync; needs 18.15+)`);
|
|
43
|
+
const stat = statfs(nearestExisting(dir));
|
|
44
|
+
return Number(stat.bavail) * Number(stat.bsize);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Bytes of every regular file under `dir`; symlinks are not followed; an absent dir is 0. */
|
|
48
|
+
export function directoryBytes(dir) {
|
|
49
|
+
let total = 0;
|
|
50
|
+
const walk = (current) => {
|
|
51
|
+
let entries;
|
|
52
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const full = path.join(current, entry.name);
|
|
55
|
+
if (entry.isDirectory()) walk(full);
|
|
56
|
+
else if (entry.isFile()) { try { total += fs.lstatSync(full).size; } catch { /* vanished */ } }
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(dir);
|
|
60
|
+
return total;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const gb = (bytes) => `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
64
|
+
|
|
65
|
+
// The ONE supported way to put the Brain on a bigger disk (CONTRIBUTING.md, "Putting the Brain on another
|
|
66
|
+
// disk"). Not RUVNET_BRAIN_HOME: an env var never reaches GUI-launched hosts or launchd, and the installer
|
|
67
|
+
// ignores it while the MCP server honours it — a split brain.
|
|
68
|
+
export const MOVE_BRAIN_HINT = 'move the Brain to a bigger disk with npx ruvnet-brain --move-brain <folder on that disk>';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `requirements`: [{ dir, bytes, purpose, brain? }]. Requirements on the same filesystem add up. A group
|
|
72
|
+
* holds the Brain unless every requirement in it says `brain: false` (e.g. a temp-dir unpack), and only
|
|
73
|
+
* such a group is offered `bigger` as the alternative to freeing space. Returns
|
|
74
|
+
* { ok, shortfalls: [{ dir, needBytes, freeBytes, shortBytes, purposes }], message }.
|
|
75
|
+
*/
|
|
76
|
+
export function checkDiskSpace(requirements, { available = availableBytes, deviceOf = (dir) => fs.statSync(nearestExisting(dir)).dev,
|
|
77
|
+
headroom = DISK_HEADROOM_BYTES, what = 'apply this update', bigger = MOVE_BRAIN_HINT } = {}) {
|
|
78
|
+
const byDevice = new Map();
|
|
79
|
+
for (const { dir, bytes, purpose, brain = true } of requirements) {
|
|
80
|
+
const device = deviceOf(dir);
|
|
81
|
+
const group = byDevice.get(device) || { dir: nearestExisting(dir), bytes: 0, purposes: [], brain: false };
|
|
82
|
+
group.bytes += bytes;
|
|
83
|
+
group.purposes.push(`${purpose} ${gb(bytes)}`);
|
|
84
|
+
group.brain ||= brain !== false;
|
|
85
|
+
byDevice.set(device, group);
|
|
86
|
+
}
|
|
87
|
+
const shortfalls = [];
|
|
88
|
+
for (const group of byDevice.values()) {
|
|
89
|
+
const needBytes = group.bytes + headroom;
|
|
90
|
+
const freeBytes = available(group.dir);
|
|
91
|
+
if (freeBytes < needBytes) shortfalls.push({ dir: group.dir, needBytes, freeBytes, shortBytes: needBytes - freeBytes, purposes: group.purposes, brain: group.brain });
|
|
92
|
+
}
|
|
93
|
+
const message = shortfalls.map((s) => `not enough free disk space to ${what}: ${s.dir} has ${gb(s.freeBytes)} free and needs `
|
|
94
|
+
+ `${gb(s.needBytes)} (${s.purposes.join(' + ')} + ${gb(headroom)} headroom). Free ${gb(s.shortBytes)} on that disk`
|
|
95
|
+
+ `${s.brain && bigger ? `, or ${bigger}` : ''}. Nothing was changed.`).join('\n');
|
|
96
|
+
return { ok: shortfalls.length === 0, shortfalls, message };
|
|
97
|
+
}
|
|
98
|
+
|
|
20
99
|
function assertDirectory(dir, label) {
|
|
21
100
|
const stat = fs.lstatSync(dir);
|
|
22
101
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`${label} is not a trusted directory: ${dir}`);
|
package/kb/zip-extract.mjs
CHANGED
|
@@ -184,6 +184,28 @@ function safeJoin(destDir, name) {
|
|
|
184
184
|
return target;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* The archive's declared total uncompressed bytes, read from its central directory only (no
|
|
189
|
+
* extraction) — what the disk-space preflight needs before anything is unpacked. The same safety
|
|
190
|
+
* limits as extractZip apply, so a hostile archive cannot make the preflight lie small.
|
|
191
|
+
*/
|
|
192
|
+
export function zipDeclaredBytes(zipPath) {
|
|
193
|
+
const fd = fs.openSync(zipPath, 'r');
|
|
194
|
+
try {
|
|
195
|
+
const size = fs.fstatSync(fd).size;
|
|
196
|
+
if (size === 0) throw new Error('archive is empty (0 bytes)');
|
|
197
|
+
const { cdSize, cdOffset } = centralDirectoryLocation(fd, size);
|
|
198
|
+
if (cdSize > ZIP_LIMITS.centralDirectoryBytes || cdOffset + cdSize > size) throw new Error('central directory is out of bounds');
|
|
199
|
+
let total = 0;
|
|
200
|
+
for (const entry of parseCentralDirectory(readAt(fd, cdSize, cdOffset))) {
|
|
201
|
+
if (!Number.isSafeInteger(entry.uncompSize) || entry.uncompSize < 0) throw new Error(`entry "${entry.name}" has an unsafe uncompressed size`);
|
|
202
|
+
total += entry.uncompSize;
|
|
203
|
+
if (total > ZIP_LIMITS.totalBytes) throw new Error(`archive claims more than ${ZIP_LIMITS.totalBytes} uncompressed bytes`);
|
|
204
|
+
}
|
|
205
|
+
return total;
|
|
206
|
+
} finally { fs.closeSync(fd); }
|
|
207
|
+
}
|
|
208
|
+
|
|
187
209
|
/**
|
|
188
210
|
* Extract `zipPath` into `destDir`, overwriting existing files (idempotent — the `-o` of
|
|
189
211
|
* `unzip -q -o`). Returns { entries, entryNames, files, bytes, crcChecked }.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain \u2014 a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
3
|
"description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.5.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Brain Console — RuvNet Brain Console (same console as /
|
|
3
|
-
updated: 2026-
|
|
2
|
+
description: "Brain Console — the RuvNet Brain Console, also opened by /rnbc (same console as /rnb, /rvbc, /rvcb and /ruvnet-brain:configure — every spelling works). Your whole RuvNet stack on one page. Read-only until you click."
|
|
3
|
+
updated: 2026-10-01
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Launch the **RuvNet Brain Console** for the user.
|
|
7
|
-
`/rvcb`,
|
|
6
|
+
Launch the **RuvNet Brain Console** (RNBC) for the user. `/rnbc` is its name; `/rnb`, `/rvbc`,
|
|
7
|
+
`/rvcb`, `/brain-console` and `/ruvnet-brain:configure` open the very same console — every spelling
|
|
8
|
+
lands here, so never tell the user they typed it wrong.
|
|
8
9
|
|
|
9
10
|
## The contract you are keeping
|
|
10
11
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Open the RuvNet Brain
|
|
3
|
-
updated: 2026-
|
|
2
|
+
description: Open the RuvNet Brain Console (/rnbc) — a local web page that mirrors YOUR machine's RuvNet setup, explains it in plain English, and lets you safely configure and fix it. Read-only until you click; every machine change is explained first and is reversible.
|
|
3
|
+
updated: 2026-10-01
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Launch the **RuvNet Brain Console** for the user.
|
|
7
|
-
`/rvcb`,
|
|
6
|
+
Launch the **RuvNet Brain Console** (RNBC) for the user. `/rnbc` is its name; `/rnb`, `/rvbc`,
|
|
7
|
+
`/rvcb`, `/brain-console` and `/ruvnet-brain:configure` open the very same console — every spelling
|
|
8
|
+
lands here, so never tell the user they typed it wrong.
|
|
8
9
|
|
|
9
10
|
## The contract you are keeping
|
|
10
11
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Come up to speed on this project from AgentDB: the brief (state, what changed, decisions, standing rules, open items, gate outcomes, agent findings) and, on request, the full recorded history. Also records a decision, lesson or open item explicitly, with a read-back receipt."
|
|
3
|
+
updated: 2026-10-01
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RuvNet-Brain: rnb-brief
|
|
7
|
+
|
|
8
|
+
The user wants to know **what has happened on this project**, from the record — not from your memory.
|
|
9
|
+
|
|
10
|
+
## Do this
|
|
11
|
+
|
|
12
|
+
**1. Show the brief.** Run it and show the output as-is:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/continuity-brief.mjs"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**2. If they asked for more** (history, "everything", one kind of thing, a time window), pull it:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/continuity-brief.mjs" --full [--kind decision|lesson|commit|release|gate|finding|open-item] [--since 7d] [--limit 200]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every line carries its provenance (the AgentDB key and time, or the commit SHA). Quote it when you
|
|
25
|
+
rely on an item. A line marked `detected` was inferred from the transcript, not stated explicitly —
|
|
26
|
+
say so. A line marked `PENDING` is durable in the outbox but not yet committed to AgentDB.
|
|
27
|
+
|
|
28
|
+
**3. If they asked you to record something** (a decision, a standing rule, an open item):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/continuity-brief.mjs" --record --kind decision|lesson|open-item --text "<exactly what was decided>" [--owner "<who>"]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Report it as stored only when it prints `stored-and-read-back`. `durable-pending` means it is safe in the
|
|
35
|
+
outbox and will commit at the next capture boundary — say exactly that, not "stored".
|
|
36
|
+
|
|
37
|
+
## What NOT to do
|
|
38
|
+
|
|
39
|
+
- Do not summarize the history from memory or from the conversation instead of running the command.
|
|
40
|
+
- Do not hand-write rows into `.swarm/memory.db` or call `ruflo memory store` for these yourself.
|
|
41
|
+
- If the last line says `AgentDB: recording stuck`, tell the user that line verbatim before anything else.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "RNB — opens the RuvNet Brain Console (same console as /rnbc, /rvbc, /brain-console and /ruvnet-brain:configure — every spelling works). Your whole RuvNet stack on one page. Read-only until you click."
|
|
3
|
+
updated: 2026-10-01
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Launch the **RuvNet Brain Console** (RNBC) for the user. `/rnbc` is its name; `/rnb`, `/rvbc`,
|
|
7
|
+
`/rvcb`, `/brain-console` and `/ruvnet-brain:configure` open the very same console — every spelling
|
|
8
|
+
lands here, so never tell the user they typed it wrong.
|
|
9
|
+
|
|
10
|
+
## The contract you are keeping
|
|
11
|
+
|
|
12
|
+
The page **opens immediately and scans itself while they watch.** The server never blocks: a cold or
|
|
13
|
+
new-project read is answered in milliseconds with "measuring…", the page keeps its skeletons and
|
|
14
|
+
narrates the wait, and each card fills in as its own measurement lands. A chip in the header says how
|
|
15
|
+
old the picture is and re-measures when clicked.
|
|
16
|
+
|
|
17
|
+
So there is nothing to wait for and nothing to warn them about. **Your entire job is: say one
|
|
18
|
+
sentence, start the server in the background, hand over the URL, and get out of the way.**
|
|
19
|
+
|
|
20
|
+
## 1. SPEAK FIRST — one short sentence, before any tool call
|
|
21
|
+
|
|
22
|
+
Your FIRST output — before any tool call, any file read, any search — is one warm line saying you are
|
|
23
|
+
opening it now and it will scan itself live in their browser. Something in the spirit of:
|
|
24
|
+
|
|
25
|
+
> "Opening your console now — it'll come up right away and scan your setup live while you watch."
|
|
26
|
+
|
|
27
|
+
Say it like a person, not a status bar. **Do not** promise 20 seconds, a minute, or any duration: the
|
|
28
|
+
page carries its own timing now, and a number invented here is a number the page will contradict.
|
|
29
|
+
|
|
30
|
+
## 2. Find the installed runtime
|
|
31
|
+
|
|
32
|
+
Use `${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
33
|
+
A current-repository copy is allowed only in an explicit developer checkout. Never guess a
|
|
34
|
+
`~/Code/ruvnet-brain` path: the installer persists this runtime for clean users.
|
|
35
|
+
|
|
36
|
+
## 3. Start it in the BACKGROUND and open the browser
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
node <repo>/scripts/onboarding-console.mjs --serve --open
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**`run_in_background: true`, ALWAYS — never in the foreground.** This is a server: it does not exit,
|
|
43
|
+
so a foreground run hangs the tool call until the 120-second timeout and turns an instant page into
|
|
44
|
+
two minutes of dead air. It binds `127.0.0.1` only and prints `http://127.0.0.1:7411/` within about a
|
|
45
|
+
third of a second.
|
|
46
|
+
|
|
47
|
+
**Then post the URL immediately.** Do not poll the log, do not wait for a scan, do not check whether
|
|
48
|
+
the data has landed — the page reports its own progress and none of it is visible from here.
|
|
49
|
+
|
|
50
|
+
**If it reports it is already running**, that is a success, not an error: open the URL yourself with
|
|
51
|
+
`open "http://127.0.0.1:<port>/"`. One honest caveat, worth half a sentence if it comes up — a
|
|
52
|
+
console that is already running is serving the project it was *launched* from, so if they want this
|
|
53
|
+
project's view they can stop that one (`^C` in its terminal) and run this again.
|
|
54
|
+
|
|
55
|
+
## 4. Hand it over and stop
|
|
56
|
+
|
|
57
|
+
Give them the URL as a clickable line, and one warm sentence: the page is **read-only until you
|
|
58
|
+
click**, every machine change is **explained first and reversible**, and Settings save at the user
|
|
59
|
+
level only. Worth naming once: the brain's on/off switch is the first card on the page, and the
|
|
60
|
+
header chip says how fresh the reading is and re-measures if they click it.
|
|
61
|
+
|
|
62
|
+
## 5. If it errors
|
|
63
|
+
|
|
64
|
+
Report it plainly — the real error, not a guess — and offer to fix it. Never leave them staring at
|
|
65
|
+
a dead tab wondering whether it is still loading.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Things NOT to do before the page is open
|
|
70
|
+
|
|
71
|
+
Each of these has cost a real user real minutes of silence, and not one of them is needed to open a
|
|
72
|
+
local web page:
|
|
73
|
+
|
|
74
|
+
- **No knowledge search, no memory recall, no source grounding, no repo exploration.** A cold
|
|
75
|
+
embedding-model load alone is allowed up to 180 seconds.
|
|
76
|
+
- **No model selection or routing work.**
|
|
77
|
+
- **No reading this repo to "check" anything first.** Find the script, run it.
|
|
78
|
+
|
|
79
|
+
Do **not** narrate the machine yourself afterwards; the page is the mirror. Your job is to open it,
|
|
80
|
+
say one sentence, and get out of the way.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "RNBC — RuvNet Brain Console. Opens the live console page: your whole RuvNet stack on one page — what's installed, what your AI learned, one-click reversible fixes. Read-only until you click. (Also: /rnb, /rvbc, /rvcb, /brain-console — all the same thing.)"
|
|
3
|
+
updated: 2026-10-01
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Launch the **RuvNet Brain Console** (RNBC) for the user. `/rnbc` is its name; `/rnb`, `/rvbc`,
|
|
7
|
+
`/rvcb`, `/brain-console` and `/ruvnet-brain:configure` open the very same console — every spelling
|
|
8
|
+
lands here, so never tell the user they typed it wrong.
|
|
9
|
+
|
|
10
|
+
## The contract you are keeping
|
|
11
|
+
|
|
12
|
+
The page **opens immediately and scans itself while they watch.** The server never blocks: a cold or
|
|
13
|
+
new-project read is answered in milliseconds with "measuring…", the page keeps its skeletons and
|
|
14
|
+
narrates the wait, and each card fills in as its own measurement lands. A chip in the header says how
|
|
15
|
+
old the picture is and re-measures when clicked.
|
|
16
|
+
|
|
17
|
+
So there is nothing to wait for and nothing to warn them about. **Your entire job is: say one
|
|
18
|
+
sentence, start the server in the background, hand over the URL, and get out of the way.**
|
|
19
|
+
|
|
20
|
+
## 1. SPEAK FIRST — one short sentence, before any tool call
|
|
21
|
+
|
|
22
|
+
Your FIRST output — before any tool call, any file read, any search — is one warm line saying you are
|
|
23
|
+
opening it now and it will scan itself live in their browser. Something in the spirit of:
|
|
24
|
+
|
|
25
|
+
> "Opening your console now — it'll come up right away and scan your setup live while you watch."
|
|
26
|
+
|
|
27
|
+
Say it like a person, not a status bar. **Do not** promise 20 seconds, a minute, or any duration: the
|
|
28
|
+
page carries its own timing now, and a number invented here is a number the page will contradict.
|
|
29
|
+
|
|
30
|
+
## 2. Find the installed runtime
|
|
31
|
+
|
|
32
|
+
Use `${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
33
|
+
A current-repository copy is allowed only in an explicit developer checkout. Never guess a
|
|
34
|
+
`~/Code/ruvnet-brain` path: the installer persists this runtime for clean users.
|
|
35
|
+
|
|
36
|
+
## 3. Start it in the BACKGROUND and open the browser
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
node <repo>/scripts/onboarding-console.mjs --serve --open
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**`run_in_background: true`, ALWAYS — never in the foreground.** This is a server: it does not exit,
|
|
43
|
+
so a foreground run hangs the tool call until the 120-second timeout and turns an instant page into
|
|
44
|
+
two minutes of dead air. It binds `127.0.0.1` only and prints `http://127.0.0.1:7411/` within about a
|
|
45
|
+
third of a second.
|
|
46
|
+
|
|
47
|
+
**Then post the URL immediately.** Do not poll the log, do not wait for a scan, do not check whether
|
|
48
|
+
the data has landed — the page reports its own progress and none of it is visible from here.
|
|
49
|
+
|
|
50
|
+
**If it reports it is already running**, that is a success, not an error: open the URL yourself with
|
|
51
|
+
`open "http://127.0.0.1:<port>/"`. One honest caveat, worth half a sentence if it comes up — a
|
|
52
|
+
console that is already running is serving the project it was *launched* from, so if they want this
|
|
53
|
+
project's view they can stop that one (`^C` in its terminal) and run this again.
|
|
54
|
+
|
|
55
|
+
## 4. Hand it over and stop
|
|
56
|
+
|
|
57
|
+
Give them the URL as a clickable line, and one warm sentence: the page is **read-only until you
|
|
58
|
+
click**, every machine change is **explained first and reversible**, and Settings save at the user
|
|
59
|
+
level only. Worth naming once: the brain's on/off switch is the first card on the page, and the
|
|
60
|
+
header chip says how fresh the reading is and re-measures if they click it.
|
|
61
|
+
|
|
62
|
+
## 5. If it errors
|
|
63
|
+
|
|
64
|
+
Report it plainly — the real error, not a guess — and offer to fix it. Never leave them staring at
|
|
65
|
+
a dead tab wondering whether it is still loading.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Things NOT to do before the page is open
|
|
70
|
+
|
|
71
|
+
Each of these has cost a real user real minutes of silence, and not one of them is needed to open a
|
|
72
|
+
local web page:
|
|
73
|
+
|
|
74
|
+
- **No knowledge search, no memory recall, no source grounding, no repo exploration.** A cold
|
|
75
|
+
embedding-model load alone is allowed up to 180 seconds.
|
|
76
|
+
- **No model selection or routing work.**
|
|
77
|
+
- **No reading this repo to "check" anything first.** Find the script, run it.
|
|
78
|
+
|
|
79
|
+
Do **not** narrate the machine yourself afterwards; the page is the mirror. Your job is to open it,
|
|
80
|
+
say one sentence, and get out of the way.
|
package/plugin/commands/rvbc.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "RvBC —
|
|
3
|
-
updated: 2026-
|
|
2
|
+
description: "RvBC — the older spelling of /rnbc, the RuvNet Brain Console (same console as /rnbc, /rnb, /brain-console and /ruvnet-brain:configure — every spelling works). Your whole RuvNet stack on one page. Read-only until you click."
|
|
3
|
+
updated: 2026-10-01
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Launch the **RuvNet Brain Console** for the user.
|
|
7
|
-
`/rvcb`,
|
|
6
|
+
Launch the **RuvNet Brain Console** (RNBC) for the user. `/rnbc` is its name; `/rnb`, `/rvbc`,
|
|
7
|
+
`/rvcb`, `/brain-console` and `/ruvnet-brain:configure` open the very same console — every spelling
|
|
8
|
+
lands here, so never tell the user they typed it wrong.
|
|
8
9
|
|
|
9
10
|
## The contract you are keeping
|
|
10
11
|
|
package/plugin/commands/rvcb.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "RvCB — RuvNet Brain Console (same console as /rvbc, /brain-console
|
|
3
|
-
updated: 2026-
|
|
2
|
+
description: "RvCB — another spelling of /rnbc, the RuvNet Brain Console (same console as /rnbc, /rnb, /rvbc, /brain-console and /ruvnet-brain:configure — every spelling works). Read-only until you click."
|
|
3
|
+
updated: 2026-10-01
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Launch the **RuvNet Brain Console** for the user.
|
|
7
|
-
`/rvcb`,
|
|
6
|
+
Launch the **RuvNet Brain Console** (RNBC) for the user. `/rnbc` is its name; `/rnb`, `/rvbc`,
|
|
7
|
+
`/rvcb`, `/brain-console` and `/ruvnet-brain:configure` open the very same console — every spelling
|
|
8
|
+
lands here, so never tell the user they typed it wrong.
|
|
8
9
|
|
|
9
10
|
## The contract you are keeping
|
|
10
11
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "What's new in RuvNet Brain — the big things in the current MAJOR release (4.0), in plain English. Not the point-release churn — the headline changes since 3.x. Ends by offering to open the Console so you can see it live."
|
|
3
|
-
updated: 2026-
|
|
3
|
+
updated: 2026-10-01
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# RuvNet-Brain: what's new
|
|
@@ -24,7 +24,7 @@ The user wants the headline story of the **major** release they're on — the 4.
|
|
|
24
24
|
The 4.0 line is where the brain got **honest, legible, fast, and self-measuring** — landing now in the
|
|
25
25
|
3.9.x releases. The big things:
|
|
26
26
|
|
|
27
|
-
- **The Console is the front door.** One live local page (type `/
|
|
27
|
+
- **The Console is the front door.** One live local page (type `/rnbc`) — your whole RuvNet stack on
|
|
28
28
|
one screen: what's installed, what the AI has actually learned from *your* projects (real memories +
|
|
29
29
|
distilled lessons, drill-down to the verbatim cards), which subscription pays for what, and one-click
|
|
30
30
|
**reversible** fixes for anything stale. New in 4.0: a plain-English explainer on every card, each
|
|
@@ -46,8 +46,8 @@ The 4.0 line is where the brain got **honest, legible, fast, and self-measuring*
|
|
|
46
46
|
**Then offer the Console (the point of the whole thing):**
|
|
47
47
|
|
|
48
48
|
End by offering to open it: *"Want me to open the Console so you can see all of this live? Just say the
|
|
49
|
-
word — or type `/
|
|
50
|
-
|
|
49
|
+
word — or type `/rnbc`."* If they say yes, follow `rnbc.md` in this same directory exactly (one warm sentence, then
|
|
50
|
+
start it in the background — the page opens at once and scans itself live; never promise a duration).
|
|
51
51
|
|
|
52
52
|
**Honesty rules for this command (same as the product):**
|
|
53
53
|
- If the installed executable fails, report its failure and do not fabricate a highlight.
|
package/plugin/mcp/server.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import os from 'node:os';
|
|
|
35
35
|
import readline from 'node:readline';
|
|
36
36
|
import { writeOwn as writeOwnReadiness } from '../scripts/mcp-readiness.mjs';
|
|
37
37
|
import { callManagedCli, MANAGED_CLI_TOOLS } from './managed-cli-interface.mjs';
|
|
38
|
+
import { unmountedNotice } from '../scripts/brain-location.mjs';
|
|
38
39
|
|
|
39
40
|
const BRAIN_HOME = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
|
|
40
41
|
const KB = process.env.RUVNET_BRAIN_KB || path.join(BRAIN_HOME, 'kb');
|
|
@@ -272,10 +273,14 @@ async function ensureChild() {
|
|
|
272
273
|
recordStartupFailure({ phase, startedAt, generation: c.generation, error: e });
|
|
273
274
|
throw new Error(`brain worker failed to initialize: ${e.message}`);
|
|
274
275
|
}
|
|
276
|
+
// kbDir: WHICH knowledge base this worker opened (ADR-0098 positive confirmation "In use") — the
|
|
277
|
+
// physical path, so a worker still serving a replaced or second copy is visible, not assumed.
|
|
278
|
+
let openedKb = KB;
|
|
279
|
+
try { openedKb = fs.realpathSync(KB); } catch { /* reported as spelled */ }
|
|
275
280
|
writeReadiness({
|
|
276
281
|
state: 'ready', phase, generation: c.generation,
|
|
277
282
|
workerPid: c.proc.pid, elapsedMs: Date.now() - startedAt,
|
|
278
|
-
retryable: false, retryState: 'none',
|
|
283
|
+
retryable: false, retryState: 'none', kbDir: openedKb,
|
|
279
284
|
});
|
|
280
285
|
armChildIdleTimer(c);
|
|
281
286
|
return c;
|
|
@@ -366,7 +371,10 @@ async function handleClient(msg) {
|
|
|
366
371
|
});
|
|
367
372
|
}
|
|
368
373
|
if (!c) {
|
|
369
|
-
|
|
374
|
+
// Moved to a disk that is not plugged in: one plain line, never 'diagnose the installation'.
|
|
375
|
+
const unmounted = unmountedNotice({ brainHome: BRAIN_HOME });
|
|
376
|
+
return clientOk(id, { content: [{ type: 'text', text: unmounted ? `search_ruvnet error: ${unmounted}`
|
|
377
|
+
: `search_ruvnet error: the brain bundle is unavailable at ${KB}. Diagnose the active installation and KB path before choosing a repair.` }], isError: true });
|
|
370
378
|
}
|
|
371
379
|
pendingCount++;
|
|
372
380
|
try {
|