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.
Files changed (102) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +612 -113
  3. package/console/app.js +178 -81
  4. package/console/index.html +1 -1
  5. package/console/install-architecture.html +1 -0
  6. package/console/scope.css +4 -1
  7. package/console/style.css +13 -0
  8. package/console/tips.html +4 -4
  9. package/kb/brain-profile.mjs +1 -0
  10. package/kb/corpus-release-identity.mjs +1 -1
  11. package/kb/forge-update.mjs +41 -17
  12. package/kb/model-requirements.mjs +4 -1
  13. package/kb/update-storage-transaction.mjs +79 -0
  14. package/kb/zip-extract.mjs +22 -0
  15. package/package.json +1 -1
  16. package/plugin/.claude-plugin/plugin.json +1 -1
  17. package/plugin/.codex-plugin/plugin.json +1 -1
  18. package/plugin/commands/brain-console.md +5 -4
  19. package/plugin/commands/configure.md +5 -4
  20. package/plugin/commands/rnb-brief.md +41 -0
  21. package/plugin/commands/rnb.md +80 -0
  22. package/plugin/commands/rnbc.md +80 -0
  23. package/plugin/commands/rvbc.md +5 -4
  24. package/plugin/commands/rvcb.md +5 -4
  25. package/plugin/commands/whats-new.md +4 -4
  26. package/plugin/mcp/server.mjs +10 -2
  27. package/plugin/scripts/advocacy-route.mjs +59 -23
  28. package/plugin/scripts/anticipate.sh +4 -0
  29. package/plugin/scripts/brain-confirmation.mjs +258 -0
  30. package/plugin/scripts/brain-footprint.mjs +494 -0
  31. package/plugin/scripts/brain-location.mjs +47 -0
  32. package/plugin/scripts/capability-registry.mjs +11 -1
  33. package/plugin/scripts/continuity-brief.mjs +324 -0
  34. package/plugin/scripts/continuity-events.mjs +327 -0
  35. package/plugin/scripts/continuity-journal.mjs +500 -0
  36. package/plugin/scripts/decision-gate.mjs +56 -3
  37. package/plugin/scripts/footprint-io.mjs +186 -0
  38. package/plugin/scripts/ground-before-write.sh +8 -1
  39. package/plugin/scripts/ground-ruvnet.sh +103 -12
  40. package/plugin/scripts/grounding-answer.mjs +2 -1
  41. package/plugin/scripts/grounding-stamp.sh +3 -0
  42. package/plugin/scripts/grounding-substance.mjs +1 -1
  43. package/plugin/scripts/grounding-turn-evidence.mjs +17 -2
  44. package/plugin/scripts/hook-input.mjs +78 -4
  45. package/plugin/scripts/kb-copy-proof.mjs +148 -0
  46. package/plugin/scripts/lesson-bridge.mjs +6 -2
  47. package/plugin/scripts/nightly-controller.mjs +8 -1
  48. package/plugin/scripts/node-sqlite.mjs +41 -0
  49. package/plugin/scripts/package-cards.json +797 -0
  50. package/plugin/scripts/package-cards.rvf +0 -0
  51. package/plugin/scripts/package-cards.rvf.idmap.json +1 -0
  52. package/plugin/scripts/package-cards.rvf.meta.json +1 -0
  53. package/plugin/scripts/package-recommender-client.mjs +138 -0
  54. package/plugin/scripts/package-recommender-flag.mjs +30 -0
  55. package/plugin/scripts/package-recommender.mjs +391 -0
  56. package/plugin/scripts/project-progression-outbox.mjs +26 -8
  57. package/plugin/scripts/project-progression-reader.mjs +4 -2
  58. package/plugin/scripts/protect-brain-state.sh +4 -1
  59. package/plugin/scripts/session-snapshot-hook.mjs +27 -3
  60. package/plugin/scripts/session-start-budget.mjs +1 -0
  61. package/plugin/scripts/session-start-core.mjs +34 -4
  62. package/plugin/scripts/session-start-health.mjs +7 -1
  63. package/plugin/scripts/session-start-update-plane.mjs +35 -0
  64. package/plugin/scripts/turn-outcome-capture.mjs +12 -1
  65. package/plugin/scripts/unprompted-runtime.mjs +2 -2
  66. package/plugin/skills/brain-console/SKILL.md +3 -3
  67. package/plugin/skills/rnbc/SKILL.md +24 -0
  68. package/plugin/skills/rvbc/SKILL.md +2 -2
  69. package/scripts/approved-runtime.mjs +2 -2
  70. package/scripts/ci/warm-brain-models.mjs +28 -0
  71. package/scripts/codex-hook-trust.mjs +94 -0
  72. package/scripts/console-runtime-identity.mjs +5 -0
  73. package/scripts/corpus-canary.mjs +46 -6
  74. package/scripts/corpus-dispatch-decision.mjs +2 -2
  75. package/scripts/corpus-promotion.mjs +1 -1
  76. package/scripts/hook-qualify-hosts.mjs +15 -3
  77. package/scripts/host-install-matrix.mjs +63 -2
  78. package/scripts/human-approval-phrases.mjs +46 -0
  79. package/scripts/installed-brain-health.mjs +53 -0
  80. package/scripts/move-brain.mjs +310 -0
  81. package/scripts/onboarding-console.mjs +93 -10
  82. package/scripts/oracle/abstain-threshold-sweep.mjs +62 -0
  83. package/scripts/oracle/abstain-trace.mjs +139 -0
  84. package/scripts/oracle/doc2query-generate.mjs +162 -0
  85. package/scripts/oracle/doc2query-reach.mjs +110 -0
  86. package/scripts/oracle/judge-train.mjs +158 -0
  87. package/scripts/oracle/need-set-split.mjs +48 -0
  88. package/scripts/oracle/sona-query-adapter-eval.mjs +139 -0
  89. package/scripts/package-cards.mjs +374 -0
  90. package/scripts/publication-receipt.mjs +37 -9
  91. package/scripts/recommendation-e2e.mjs +110 -0
  92. package/scripts/recommendation-eval.mjs +105 -0
  93. package/scripts/recommendation-floor.mjs +56 -0
  94. package/scripts/recommendation-judge-score.mjs +74 -0
  95. package/scripts/recommendation-latency.mjs +95 -0
  96. package/scripts/recommendation-real-host-score.mjs +76 -0
  97. package/scripts/recommendation-real-host.mjs +137 -0
  98. package/scripts/release-channel-kind.mjs +1 -1
  99. package/scripts/release-environment-policy.mjs +33 -0
  100. package/scripts/single-source-check.mjs +15 -10
  101. package/scripts/sync-commands.mjs +5 -2
  102. package/scripts/wired-check.mjs +17 -2
@@ -0,0 +1,148 @@
1
+ // DISTINCT-FROM: kb/forge-update.mjs reclaimBackups/assertRedundantBackup — that proof requires every byte of a copy to survive in live, which no older generation satisfies; this one requires byte-identity only for private-store files and public provenance for stores the live brain lacks (ADR-0098).
2
+ //
3
+ // kb-copy-proof.mjs — may a full copy of the knowledge base be deleted? The ONE proof used by the
4
+ // footprint sweep (plugin/scripts/brain-footprint.mjs) and by the installer right after it activates a
5
+ // new generation (bin/install.mjs unzipInto). Pure read; never follows a link; never writes.
6
+ //
7
+ // A copy is DISPOSABLE only when nothing in it is unique. Every file must be one of:
8
+ // * a PRIVATE-store file (names from the PRIVATE-STORES.json fence of the live brain AND of the copy, plus
9
+ // every updateManaged:false store in either SOURCE.json; membership rule = kb/forge-update.mjs
10
+ // capturePrivateOverlayState) that exists BYTE-IDENTICAL at the same path in the live brain — nothing
11
+ // else excuses a private file;
12
+ // * a public release file: listed with these exact bytes in the copy's own ARCHIVE-MANIFEST.json, or a
13
+ // name the live generation ships, or a member of a public store family (named by either COVERAGE.json
14
+ // or the live SOURCE.json) that a newer release replaced or retired;
15
+ // * installer-written or reinstallable (node_modules/, the updater/validator files the installer places);
16
+ // * a symbolic link identical in the live brain (links are compared, never followed).
17
+ // Anything else — a user's own file, an unfenced store, a link the live brain lacks — KEEPS the copy, and
18
+ // is named. The live brain itself must be present, so a copy is never removed while it may be the only one.
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import crypto from 'node:crypto';
22
+ import { isVolumeMetadata } from './footprint-io.mjs';
23
+
24
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
25
+ const lstat = (file) => { try { return fs.lstatSync(file); } catch { return null; } };
26
+ const names = (dir) => { try { return fs.readdirSync(dir).sort(); } catch { return []; } };
27
+ const sha256File = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
28
+
29
+ /** Every regular file and link under `root`, relative, without following links. macOS volume metadata
30
+ * (AppleDouble `._*` shadows on an exFAT disk, .DS_Store, …) is the volume's, never a copy's unique data. */
31
+ function walk(root, prefix = '', out = []) {
32
+ for (const name of names(path.join(root, prefix)).filter((n) => !isVolumeMetadata(n))) {
33
+ const relative = prefix ? path.join(prefix, name) : name;
34
+ const st = lstat(path.join(root, relative));
35
+ if (!st) continue;
36
+ if (st.isSymbolicLink()) out.push({ relative, link: true });
37
+ else if (st.isDirectory()) walk(root, relative, out);
38
+ else if (st.isFile()) out.push({ relative, link: false, size: st.size });
39
+ }
40
+ return out;
41
+ }
42
+
43
+ const storeList = (source) => {
44
+ const stores = source?.stores;
45
+ if (Array.isArray(stores)) return stores;
46
+ return stores && typeof stores === 'object' ? Object.entries(stores).map(([kbName, value]) => ({ kbName, ...value })) : [];
47
+ };
48
+ const stemOf = (file) => path.basename(String(file)).replace(/(?:\.big)?\.rvf$/i, '').toLowerCase();
49
+ // The store a sidecar belongs to: <store>.big.rvf[.embed|.idmap].json, <store>.meta.json, <store>.passages.jsonl,
50
+ // <store>.symbols.json, <store>-primer.md.
51
+ const storeStem = (file) => path.basename(String(file)).toLowerCase()
52
+ .replace(/-primer\.md$/, '').replace(/(?:\.big)?\.rvf(?:\.[a-z]+\.json)?$/, '').replace(/\.(?:meta|symbols)\.json$|\.passages\.jsonl$/, '');
53
+ // Files the installer/updater writes into a KB that no bundle ships (bin/install.mjs placeUpdater,
54
+ // placeTrustedCoverageValidator, ensureVerifier; the updater's snapshot receipt). Re-created on every install.
55
+ const INSTALLER_WRITTEN = new Set(['coverage-integrity.mjs', 'RUNTIME-IDENTITY.json', '.refresh-snapshot.json',
56
+ 'forge-update.mjs', 'zip-extract.mjs', 'brain-profile.mjs', 'refresh-run.mjs', 'update-storage-transaction.mjs',
57
+ 'lifecycle-evidence-retention.mjs', 'corpus-release-identity.mjs', 'verify-citation.mjs', 'package-lock.json', '.DS_Store']);
58
+
59
+ /** A directory that is a knowledge-base tree (SOURCE.json plus a store or the search entry point). */
60
+ export const isKbTree = (dir) => Boolean(lstat(path.join(dir, 'SOURCE.json')))
61
+ && names(dir).some((n) => /\.rvf$/i.test(n) || n === 'forge-mcp-all.mjs');
62
+
63
+ /** Private store names the given trees declare: PRIVATE-STORES.json fences and updateManaged:false stores. */
64
+ export function privateStoreNames(dirs) {
65
+ const out = new Set();
66
+ for (const dir of dirs) {
67
+ const fence = readJson(path.join(dir, 'PRIVATE-STORES.json'))?.privateStores;
68
+ for (const name of Array.isArray(fence) ? fence : []) out.add(String(name).toLowerCase());
69
+ for (const store of storeList(readJson(path.join(dir, 'SOURCE.json')))) {
70
+ if (store?.updateManaged === false && store.kbName) out.add(String(store.kbName).toLowerCase());
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** The capturePrivateOverlayState membership rule (kb/forge-update.mjs), applied to one relative path. */
77
+ function belongsToPrivate(relative, privateNames, privateArtifacts) {
78
+ const base = path.basename(relative).toLowerCase();
79
+ for (const name of privateNames) if (base === name || base.startsWith(`${name}.`) || base.startsWith(`${name}-`)) return true;
80
+ return privateArtifacts.some(({ directory, basename, stem }) => path.dirname(relative) === directory
81
+ && (base === basename || base.startsWith(`${basename}.`) || base.startsWith(`${stem}.`) || base.startsWith(`${stem}-`)));
82
+ }
83
+
84
+ const sameBytes = (left, right) => {
85
+ const a = lstat(left); const b = lstat(right);
86
+ if (!a || !b || !a.isFile() || !b.isFile() || a.isSymbolicLink() || b.isSymbolicLink() || a.size !== b.size) return false;
87
+ try { return sha256File(left) === sha256File(right); } catch { return false; }
88
+ };
89
+
90
+ /**
91
+ * @returns {{disposable: boolean, unique: {file: string, why: string}[], reason: string}} — a kept copy
92
+ * always names the files that keep it.
93
+ */
94
+ export function kbCopyProof({ copyDir, liveDir }) {
95
+ const copy = lstat(copyDir);
96
+ if (!copy || copy.isSymbolicLink() || !copy.isDirectory()) return { disposable: false, unique: [], reason: 'not a real directory (a link is never entered)' };
97
+ const live = lstat(liveDir);
98
+ if (!live || !live.isDirectory() || !readJson(path.join(liveDir, 'SOURCE.json'))
99
+ || !names(liveDir).some((name) => /\.rvf$/i.test(name))) {
100
+ return { disposable: false, unique: [], reason: 'the live brain is missing or incomplete, so this copy may be the only good one' };
101
+ }
102
+ const privateNames = privateStoreNames([liveDir, copyDir]);
103
+ const generations = readJson(path.join(copyDir, 'RVF-GENERATIONS.json'))?.stores || {};
104
+ const privateArtifacts = Object.entries(generations).filter(([name]) => privateNames.has(name.toLowerCase()))
105
+ .map(([, g]) => String(g?.file || '')).filter(Boolean).map((file) => ({ directory: path.dirname(path.normalize(file)),
106
+ basename: path.basename(file).toLowerCase(), stem: stemOf(file) }));
107
+ const coverageNames = (dir) => (readJson(path.join(dir, 'COVERAGE.json'))?.rows || [])
108
+ .flatMap((row) => [row?.artifact?.store, row?.name]).filter(Boolean).map((n) => String(n).toLowerCase());
109
+ const publicNames = new Set([...coverageNames(liveDir), ...coverageNames(copyDir),
110
+ ...storeList(readJson(path.join(liveDir, 'SOURCE.json'))).filter((s) => s?.updateManaged !== false)
111
+ .map((s) => String(s.kbName || '').toLowerCase())]);
112
+ for (const name of privateNames) publicNames.delete(name); // a fenced name is never "public provenance"
113
+ // The copy's own signed release manifest (scripts/build-bundle.mjs, every bundle since 2026-08-23): a file
114
+ // it lists with these exact bytes is a public, re-downloadable release file.
115
+ const shipped = new Map((readJson(path.join(copyDir, 'ARCHIVE-MANIFEST.json'))?.files || [])
116
+ .filter((f) => typeof f?.path === 'string').map((f) => [path.normalize(f.path), f]));
117
+ const unique = [];
118
+ let files;
119
+ try { files = walk(copyDir); } catch (error) { return { disposable: false, unique, reason: `unreadable copy: ${error.message}` }; }
120
+ for (const { relative, link, size } of files) {
121
+ // node_modules (npm reinstalls it) and .console-runtime (bin/install.mjs installConsoleRuntime re-places it)
122
+ if (['node_modules', '.console-runtime'].includes(relative.split(path.sep)[0])
123
+ || (!relative.includes(path.sep) && INSTALLER_WRITTEN.has(relative))) continue;
124
+ const isPrivate = belongsToPrivate(relative, privateNames, privateArtifacts);
125
+ const inLive = path.join(liveDir, relative);
126
+ if (link) {
127
+ const liveLink = lstat(inLive);
128
+ const same = liveLink?.isSymbolicLink() && fs.readlinkSync(inLive) === fs.readlinkSync(path.join(copyDir, relative));
129
+ if (!same) unique.push({ file: relative, why: 'a symbolic link the live brain does not have (never followed)' });
130
+ continue;
131
+ }
132
+ if (isPrivate) {
133
+ if (!sameBytes(path.join(copyDir, relative), inLive)) {
134
+ unique.push({ file: relative, why: lstat(inLive) ? 'private file differs from the live brain' : 'private file absent from the live brain' });
135
+ }
136
+ continue;
137
+ }
138
+ if (relative === 'ARCHIVE-MANIFEST.json' && shipped.size) continue; // the release manifest itself
139
+ const listed = shipped.get(relative);
140
+ if (listed && listed.bytes === size && listed.sha256 === sha256File(path.join(copyDir, relative))) continue;
141
+ if (lstat(inLive)) continue; // a release-owned name the live generation ships (same or newer bytes)
142
+ if (publicNames.has(storeStem(relative))) continue; // a public store family a newer release replaced or retired
143
+ unique.push({ file: relative, why: 'not in the live brain, not in this copy\'s release manifest, not a public store' });
144
+ }
145
+ return unique.length
146
+ ? { disposable: false, unique, reason: `holds ${unique.length} file(s) the live brain does not: ${unique.slice(0, 4).map((u) => u.file).join(', ')}${unique.length > 4 ? ', …' : ''}` }
147
+ : { disposable: true, unique, reason: 'every private file is byte-identical in the live brain; public bytes are re-downloadable' };
148
+ }
@@ -23,6 +23,7 @@ import path from 'node:path';
23
23
  import { execFileSync } from 'node:child_process';
24
24
  import { fileURLToPath } from 'node:url';
25
25
  import { makeLesson, updateLessons, loadLessons, ENFORCEMENT, ORIGIN, STATUS, TRIGGERS } from './lesson-store.mjs';
26
+ import { loadNodeSqlite } from './node-sqlite.mjs';
26
27
 
27
28
  /** Every bridged lesson id starts with this. It is how a merge knows which rows it owns. */
28
29
  export const BRIDGE_PREFIX = 'G-';
@@ -108,8 +109,11 @@ export function readProjectRows(dbPath = PROJECT_DB) {
108
109
  } catch { return []; }
109
110
  }
110
111
 
111
- /** Indirection so a missing node:sqlite is a caught throw rather than a module-load crash. */
112
- function require$(id) { return process.getBuiltinModule ? process.getBuiltinModule(id) : null; }
112
+ /**
113
+ * Indirection so a missing node:sqlite is a caught throw rather than a module-load crash, loaded
114
+ * through node-sqlite.mjs so Node 22's SQLite ExperimentalWarning never reaches hook output.
115
+ */
116
+ function require$(id) { return id === 'node:sqlite' ? loadNodeSqlite() : (process.getBuiltinModule ? process.getBuiltinModule(id) : null); }
113
117
 
114
118
  // ── Row → lesson ─────────────────────────────────────────────────────────────────────────────────
115
119
 
@@ -73,6 +73,11 @@ export function nightlyStatus(options = {}) {
73
73
  testMode: options.testMode ?? env.RUVNET_BRAIN_SCHEDULER_TEST === '1' });
74
74
  }
75
75
 
76
+ export function stripAnsi(text) {
77
+ // eslint-disable-next-line no-control-regex
78
+ return String(text).replace(/\u001b\[[0-9;]*[A-Za-z]/g, '');
79
+ }
80
+
76
81
  export function applyNightlyChoice(enabled, options = {}) {
77
82
  if (typeof enabled !== 'boolean') return { ok: false, log: 'nightly must be true or false' };
78
83
  const env = schedulerEnvironment(options.env || process.env);
@@ -97,6 +102,8 @@ export function applyNightlyChoice(enabled, options = {}) {
97
102
  after,
98
103
  log: ok
99
104
  ? `Nightly refresh is ${desired}; ${after.evidence}.`
100
- : `Nightly refresh did not reach ${desired}: ${run.error?.message || run.stderr?.trim() || run.stdout?.trim() || `exit ${run.status}`}`,
105
+ // The installer colours its terminal output; this string is shown in the console page, where
106
+ // raw ANSI escapes render as "[31m✗" noise (RNBC QA 2026-10-01). Strip them here, once.
107
+ : `Nightly refresh did not reach ${desired}: ${stripAnsi(run.error?.message || run.stderr?.trim() || run.stdout?.trim() || `exit ${run.status}`)}`,
101
108
  };
102
109
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * node-sqlite.mjs — the ONE way plugin code loads `node:sqlite`, lazily and without hook noise.
3
+ *
4
+ * WHY (measured 2026-10-01, Node 22.13.1 locally and Node 22 on the Linux CI full suite, run 36894944735):
5
+ * on Node 22 the first load of `node:sqlite` prints
6
+ * (node:PID) ExperimentalWarning: SQLite is an experimental feature and might change at any time
7
+ * to stderr. SessionStart reads the store through project-progression-reader.mjs, so every SessionStart
8
+ * in an adopted git project on Node 22 carried that line — hook noise a customer sees on every session.
9
+ * Node 24 does not warn, which is why the owner's machine never showed it.
10
+ *
11
+ * WHAT: the module is required only when a caller actually needs it (never at import time), and during
12
+ * that one synchronous require `process.emitWarning` drops exactly ONE warning shape — type
13
+ * ExperimentalWarning whose message names SQLite — and passes every other warning through untouched.
14
+ * The original `process.emitWarning` is restored before returning, whether the require worked or not.
15
+ * Node's emitExperimentalWarning calls the public process.emitWarning synchronously while the builtin
16
+ * is being compiled (node:internal/util emitExperimentalWarning), which is what makes the scope exact.
17
+ */
18
+ import { createRequire } from 'node:module';
19
+
20
+ const requireBuiltin = createRequire(import.meta.url);
21
+ let loaded;
22
+
23
+ /** True for the one warning this module exists to keep out of hook output. */
24
+ export function isSqliteExperimentalWarning(warning, typeOrOptions) {
25
+ const type = typeof typeOrOptions === 'string' ? typeOrOptions : typeOrOptions?.type;
26
+ const name = warning && typeof warning === 'object' ? warning.name : undefined;
27
+ const message = typeof warning === 'string' ? warning : warning?.message;
28
+ return (type === 'ExperimentalWarning' || name === 'ExperimentalWarning') && /\bSQLite\b/.test(String(message ?? ''));
29
+ }
30
+
31
+ /** The `node:sqlite` module, or null where this Node has none. Loaded once; never warns about SQLite. */
32
+ export function loadNodeSqlite({ load = (id) => requireBuiltin(id) } = {}) {
33
+ if (loaded !== undefined) return loaded;
34
+ const original = process.emitWarning;
35
+ process.emitWarning = function emitWarningExceptSqlite(warning, ...rest) {
36
+ if (isSqliteExperimentalWarning(warning, rest[0])) return undefined;
37
+ return original.call(this, warning, ...rest);
38
+ };
39
+ try { loaded = load('node:sqlite'); } catch { loaded = null; } finally { process.emitWarning = original; }
40
+ return loaded;
41
+ }