ruvnet-brain 4.0.4 → 4.0.5
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 +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/lesson-command-scope.mjs +133 -0
- package/plugin/scripts/lesson-gate.mjs +401 -0
- package/plugin/scripts/lesson-presentation.mjs +99 -0
- package/plugin/scripts/lesson-store.mjs +452 -0
- package/plugin/scripts/route-dispatch.sh +1 -0
- package/plugin/scripts/verify-interface.sh +1 -0
- package/scripts/learning-replay-cli.mjs +236 -0
- package/scripts/learning-replay-contract.mjs +255 -0
- package/scripts/learning-replay-execution.mjs +193 -0
- package/scripts/learning-replay-fixture.mjs +380 -0
- package/scripts/learning-replay-proof.mjs +459 -0
- package/scripts/learning-replay.mjs +10 -1565
- package/scripts/lesson-gate.mjs +3 -679
- package/scripts/lesson-store.mjs +4 -447
- package/scripts/memory-doctor.mjs +19 -3
- package/scripts/onboarding-console.mjs +77 -43
- package/scripts/release-vector.mjs +49 -25
|
@@ -69,23 +69,44 @@ import { applyNightlyChoice, nightlyStatus } from './nightly-controller.mjs';
|
|
|
69
69
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
70
70
|
const REPO = path.dirname(__dirname);
|
|
71
71
|
const CONSOLE_DIR = path.join(REPO, 'console');
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
const SYSTEM_HOME = os.homedir();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The console's explicit root boundary. It scopes only console-owned configuration, state, cache,
|
|
76
|
+
* and discovery; global binaries, account credentials, and external tool state retain SYSTEM_HOME.
|
|
77
|
+
*/
|
|
78
|
+
export function consoleRootFromEnvironment(env = process.env, systemHome = SYSTEM_HOME) {
|
|
79
|
+
const configured = env.RUVNET_CONSOLE_ROOT;
|
|
80
|
+
if (!configured) return systemHome;
|
|
81
|
+
if (typeof configured !== 'string' || !path.isAbsolute(configured)) {
|
|
82
|
+
throw new Error('RUVNET_CONSOLE_ROOT must be an absolute path');
|
|
83
|
+
}
|
|
84
|
+
return path.resolve(configured);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A test or an embedded caller can isolate the console's owned files and discovery roots without
|
|
88
|
+
// redefining the operating-system home directory. Normal launches retain the OS home exactly.
|
|
89
|
+
const CONSOLE_ROOT = consoleRootFromEnvironment();
|
|
90
|
+
const NPM_PREFIX = path.join(SYSTEM_HOME, '.npm-global');
|
|
91
|
+
const CONFIG_DIR = path.join(CONSOLE_ROOT, '.claude/ruvnet-brain');
|
|
75
92
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
76
|
-
const UNDO_JOURNAL = path.join(
|
|
93
|
+
const UNDO_JOURNAL = path.join(CONSOLE_ROOT, '.cache/ruvnet-brain/console-undo.jsonl');
|
|
77
94
|
const INSTALLED_KB = process.env.RUVNET_BRAIN_KB
|
|
78
|
-
|| path.join(
|
|
95
|
+
|| path.join(CONSOLE_ROOT, '.cache', 'ruvnet-brain', 'kb');
|
|
79
96
|
const COMPLETE_BRAIN_SOURCE = process.env.RUVNET_BRAIN_COMPLETE_SOURCE
|
|
80
97
|
|| path.join(REPO, 'dist', 'ruvnet-brain');
|
|
81
98
|
const TOKEN = crypto.randomBytes(24).toString('hex');
|
|
82
|
-
const RUNTIME_RECEIPT_DIR = path.join(
|
|
99
|
+
const RUNTIME_RECEIPT_DIR = path.join(CONSOLE_ROOT, '.cache', 'ruvnet-brain', 'console-instances');
|
|
83
100
|
const RUNTIME_PRODUCT = 'ruvnet-brain-console';
|
|
84
101
|
const RUNTIME_SCHEMA = 1;
|
|
85
102
|
const RUNTIME_API_CONTRACT = 1;
|
|
86
103
|
const RUNTIME_SCRIPT = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
87
104
|
const RUNTIME_SOURCE_SHA256 = crypto.createHash('sha256').update(fs.readFileSync(RUNTIME_SCRIPT)).digest('hex');
|
|
88
105
|
|
|
106
|
+
function consoleCandidateRoots() {
|
|
107
|
+
return candidateRoots({ home: CONSOLE_ROOT, configPath: CONFIG_PATH });
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
function canonicalScope(cwd = process.cwd()) {
|
|
90
111
|
try { return fs.realpathSync(cwd); } catch { return path.resolve(cwd); }
|
|
91
112
|
}
|
|
@@ -356,7 +377,7 @@ function wiringSurvey() {
|
|
|
356
377
|
// must never count the same project twice.
|
|
357
378
|
const seenProjects = new Set();
|
|
358
379
|
const projects = [];
|
|
359
|
-
for (const root of
|
|
380
|
+
for (const root of consoleCandidateRoots()) {
|
|
360
381
|
for (const proj of findProjects(root)) {
|
|
361
382
|
const resolved = path.resolve(proj);
|
|
362
383
|
if (seenProjects.has(resolved)) continue;
|
|
@@ -394,7 +415,7 @@ function wiringSurvey() {
|
|
|
394
415
|
|
|
395
416
|
// ── Memory health (read-only probes for the project the console was launched from) ────────────────
|
|
396
417
|
function sessionHookExists() {
|
|
397
|
-
return fs.existsSync(path.join(
|
|
418
|
+
return fs.existsSync(path.join(CONSOLE_ROOT, '.claude/hooks/agentdb-ensure.sh')) || fs.existsSync(path.join(CONSOLE_ROOT, '.claude/hooks'));
|
|
398
419
|
}
|
|
399
420
|
function probeMemory(projectDir) {
|
|
400
421
|
const db = path.join(projectDir, '.swarm/memory.db');
|
|
@@ -462,7 +483,7 @@ function scanFleet() {
|
|
|
462
483
|
function gatherMemory(cwd, { fleet = true } = {}) {
|
|
463
484
|
// health = for the project the console was launched from (fall back to this repo)
|
|
464
485
|
const project = fs.existsSync(path.join(cwd, '.swarm/memory.db')) ? cwd : REPO;
|
|
465
|
-
const projName = project.replace(
|
|
486
|
+
const projName = project.replace(CONSOLE_ROOT + '/Code/', '').replace(CONSOLE_ROOT + '/', '~/');
|
|
466
487
|
const health = scoreMemoryHealth({ project: projName, probes: probeMemory(project) });
|
|
467
488
|
return { fleet: fleet ? scanFleet() : null, health };
|
|
468
489
|
}
|
|
@@ -471,10 +492,10 @@ function gatherMemory(cwd, { fleet = true } = {}) {
|
|
|
471
492
|
function gatherSavings() {
|
|
472
493
|
// Primary source is the real routing-receipts ledger written by scripts/route-cheap.mjs.
|
|
473
494
|
const files = [
|
|
474
|
-
path.join(
|
|
475
|
-
path.join(
|
|
495
|
+
path.join(SYSTEM_HOME, '.claude/metaharness/routing-receipts.jsonl'),
|
|
496
|
+
path.join(SYSTEM_HOME, '.cache/ruvnet-brain/metaharness-receipts.jsonl'),
|
|
476
497
|
// Canonical user-level ledger (issue #36 — the hooks no longer scatter per-CWD copies).
|
|
477
|
-
path.join(
|
|
498
|
+
path.join(SYSTEM_HOME, '.cache/ruvnet-brain/token-ledger.jsonl'),
|
|
478
499
|
// Legacy location, still read so an existing user's history is not orphaned by the move.
|
|
479
500
|
path.join(REPO, 'plugin/scripts/.ruvnet-brain/token-ledger.jsonl'),
|
|
480
501
|
];
|
|
@@ -578,7 +599,7 @@ function gatherConfig() {
|
|
|
578
599
|
});
|
|
579
600
|
}
|
|
580
601
|
return {
|
|
581
|
-
path: CONFIG_PATH.replace(
|
|
602
|
+
path: CONFIG_PATH.replace(CONSOLE_ROOT, '~'),
|
|
582
603
|
exists: fs.existsSync(CONFIG_PATH),
|
|
583
604
|
values: {
|
|
584
605
|
openrouterKey: credential.configured, // boolean only — never the secret itself
|
|
@@ -627,7 +648,7 @@ function gatherAdvocacy() {
|
|
|
627
648
|
const chosen = raw && typeof raw === 'object' && raw.settings && typeof raw.settings === 'object'
|
|
628
649
|
? raw.settings : {};
|
|
629
650
|
return {
|
|
630
|
-
path: state.path.replace(
|
|
651
|
+
path: state.path.replace(CONSOLE_ROOT, '~'),
|
|
631
652
|
exists: state.exists,
|
|
632
653
|
values: Object.fromEntries(LIVE_USER_FIELDS.map((field) => [
|
|
633
654
|
field.key,
|
|
@@ -675,7 +696,7 @@ function saveAdvocacy(values) {
|
|
|
675
696
|
publishSettingsToCache();
|
|
676
697
|
return {
|
|
677
698
|
ok: true,
|
|
678
|
-
backup: result.backup ? result.backup.replace(
|
|
699
|
+
backup: result.backup ? result.backup.replace(CONSOLE_ROOT, '~') : null,
|
|
679
700
|
values: Object.fromEntries(LIVE_USER_SETTING_KEYS.map((key) => [key, result.values[key]])),
|
|
680
701
|
log: result.log,
|
|
681
702
|
};
|
|
@@ -715,7 +736,7 @@ function gatherBrainProfile() {
|
|
|
715
736
|
const source = measureBrainProfile(COMPLETE_BRAIN_SOURCE);
|
|
716
737
|
const updaterAvailable = fs.existsSync(path.join(INSTALLED_KB, 'forge-update.mjs'));
|
|
717
738
|
return {
|
|
718
|
-
path: INSTALLED_KB.replace(
|
|
739
|
+
path: INSTALLED_KB.replace(CONSOLE_ROOT, '~'),
|
|
719
740
|
values: { brainProfile: actual },
|
|
720
741
|
stored: settings.values.brainProfile,
|
|
721
742
|
disagreement: settings.values.brainProfile !== actual,
|
|
@@ -736,7 +757,7 @@ function gatherBrainProfile() {
|
|
|
736
757
|
bytes: installed.byStore.ruvector ?? source.byStore.ruvector ?? null,
|
|
737
758
|
},
|
|
738
759
|
},
|
|
739
|
-
restoreSource: COMPLETE_BRAIN_SOURCE.replace(
|
|
760
|
+
restoreSource: COMPLETE_BRAIN_SOURCE.replace(CONSOLE_ROOT, '~'),
|
|
740
761
|
};
|
|
741
762
|
}
|
|
742
763
|
|
|
@@ -747,7 +768,7 @@ function gatherBrainPower() {
|
|
|
747
768
|
off: state.off,
|
|
748
769
|
since: state.since,
|
|
749
770
|
reason: state.reason,
|
|
750
|
-
switchPath: state.path.replace(
|
|
771
|
+
switchPath: state.path.replace(CONSOLE_ROOT, '~'),
|
|
751
772
|
// The RESOLVED answer — what the machine actually does — not the mirror's opinion of it.
|
|
752
773
|
values: { brainEnabled: !state.off },
|
|
753
774
|
defaults: { brainEnabled: BRAIN_FIELD.default },
|
|
@@ -829,7 +850,7 @@ function saveBrainProfile(values) {
|
|
|
829
850
|
removed: changed.removed || [],
|
|
830
851
|
bytesFreed: changed.bytesFreed || 0,
|
|
831
852
|
mirrored: mirrored.ok,
|
|
832
|
-
backup: mirrored.backup ? mirrored.backup.replace(
|
|
853
|
+
backup: mirrored.backup ? mirrored.backup.replace(CONSOLE_ROOT, '~') : null,
|
|
833
854
|
log: mirrored.ok
|
|
834
855
|
? (profile === PROFILE_RUVECTOR
|
|
835
856
|
? `RuVector Only is active — ${changed.removed.length} unselected artifact(s) removed`
|
|
@@ -917,7 +938,7 @@ function saveBrainPower(values) {
|
|
|
917
938
|
ok: true,
|
|
918
939
|
off: state.off,
|
|
919
940
|
since: state.since,
|
|
920
|
-
backup: mirrored.backup ? mirrored.backup.replace(
|
|
941
|
+
backup: mirrored.backup ? mirrored.backup.replace(CONSOLE_ROOT, '~') : null,
|
|
921
942
|
values: { brainEnabled: !state.off },
|
|
922
943
|
// Honest about the half-failure rather than reporting a clean success: the switch is what counts
|
|
923
944
|
// and it moved, but say so plainly if the visible record did not follow it.
|
|
@@ -1087,7 +1108,7 @@ function setLesson(body) {
|
|
|
1087
1108
|
// setImmediate either (deferring synchronous work still blocks the loop when it runs). A machine with
|
|
1088
1109
|
// no fleet scan yet is reported `warming`, never as a fabricated zero.
|
|
1089
1110
|
let ACTIVITY_MACHINE_CACHE = null;
|
|
1090
|
-
const CONSOLE_CACHE_PATH = path.join(
|
|
1111
|
+
const CONSOLE_CACHE_PATH = path.join(CONSOLE_ROOT, '.cache/ruvnet-brain/console-cache.json');
|
|
1091
1112
|
|
|
1092
1113
|
// ── Warm-cache serving (2026-07-17, the demo-hang fix) ─────────────────────────────────────────────
|
|
1093
1114
|
// Every read-model here does multi-second synchronous work: gatherState ~13s, gatherStack ~22s,
|
|
@@ -1537,7 +1558,7 @@ function refreshFleetCache() {
|
|
|
1537
1558
|
const seen = new Set();
|
|
1538
1559
|
// Scan every candidate root (issue #19) — this is what made machine-wide totals read 0 on a
|
|
1539
1560
|
// machine whose projects live under ~/source instead of ~/Code.
|
|
1540
|
-
for (const root of
|
|
1561
|
+
for (const root of consoleCandidateRoots()) {
|
|
1541
1562
|
for (const s of findMemoryStores(root)) {
|
|
1542
1563
|
const resolved = path.resolve(s.project);
|
|
1543
1564
|
if (seen.has(resolved)) continue; // a project visible under two roots (e.g. a symlink) counts once
|
|
@@ -1816,11 +1837,11 @@ function brainVersionOnDisk() {
|
|
|
1816
1837
|
try { return getVersion(); } catch { return null; }
|
|
1817
1838
|
}
|
|
1818
1839
|
function readInstallChannel() {
|
|
1819
|
-
const reg = readJSON(path.join(
|
|
1840
|
+
const reg = readJSON(path.join(SYSTEM_HOME, '.claude/plugins/installed_plugins.json'));
|
|
1820
1841
|
const entries = reg && reg.plugins && reg.plugins['ruvnet-brain@ruvnet-brain'];
|
|
1821
1842
|
const e = Array.isArray(entries) ? entries[0] : null;
|
|
1822
1843
|
if (!e || !e.installPath || !fs.existsSync(e.installPath)) return { installed: false };
|
|
1823
|
-
const km = readJSON(path.join(
|
|
1844
|
+
const km = readJSON(path.join(SYSTEM_HOME, '.claude/plugins/known_marketplaces.json'));
|
|
1824
1845
|
const src = km && km['ruvnet-brain'] && km['ruvnet-brain'].source;
|
|
1825
1846
|
const pinned = !!(src && (src.ref || src.tag || src.commit)); // no pin recorded → tracking latest
|
|
1826
1847
|
return {
|
|
@@ -1828,7 +1849,7 @@ function readInstallChannel() {
|
|
|
1828
1849
|
version: path.basename(e.installPath) || e.version || null, // the plugin cache version dir IS the truth
|
|
1829
1850
|
channel: pinned ? 'pinned' : 'latest',
|
|
1830
1851
|
lastUpdated: e.lastUpdated || null,
|
|
1831
|
-
cacheDir: String(e.installPath).replace(
|
|
1852
|
+
cacheDir: String(e.installPath).replace(SYSTEM_HOME, '~'),
|
|
1832
1853
|
repo: (src && src.repo) || null,
|
|
1833
1854
|
};
|
|
1834
1855
|
}
|
|
@@ -1969,7 +1990,7 @@ function gatherState(cwd, { fleet = true } = {}) {
|
|
|
1969
1990
|
// agentic-flow, and the Savings card claiming "Smart routing: ON" while the Capabilities card said
|
|
1970
1991
|
// "not installed" — in the same render — was the contradiction that made the whole page untrustworthy.
|
|
1971
1992
|
// The card now carries the same measurement the capability row uses, so the two cannot disagree.
|
|
1972
|
-
savings.routingInstalled = fs.existsSync(path.join(
|
|
1993
|
+
savings.routingInstalled = fs.existsSync(path.join(SYSTEM_HOME, '.npm-global/bin/agentic-flow'));
|
|
1973
1994
|
try { savings.routerEngine = gatherRouterEngine(); } catch { savings.routerEngine = null; }
|
|
1974
1995
|
try {
|
|
1975
1996
|
const cat = loadCatalog();
|
|
@@ -2015,7 +2036,7 @@ function gatherState(cwd, { fleet = true } = {}) {
|
|
|
2015
2036
|
token: TOKEN,
|
|
2016
2037
|
generatedAt: new Date().toISOString(),
|
|
2017
2038
|
preStateHash,
|
|
2018
|
-
host: { user: os.userInfo().username, platform: process.platform, node: process.version, npmPrefix: NPM_PREFIX.replace(
|
|
2039
|
+
host: { user: os.userInfo().username, platform: process.platform, node: process.version, npmPrefix: NPM_PREFIX.replace(SYSTEM_HOME, '~'), brainVersion: brainVersionOnDisk() },
|
|
2019
2040
|
sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations },
|
|
2020
2041
|
};
|
|
2021
2042
|
// Cache the last good state so repeat page-loads paint instantly, same as the stack audit does.
|
|
@@ -2036,7 +2057,7 @@ function gatherStack() {
|
|
|
2036
2057
|
// ISSUE #22 — carry `source` ('npm-global' | 'plugin') + marketplace through so the console can show
|
|
2037
2058
|
// (and count) tools installed via the Claude Code plugin marketplace, not just `npm install -g` ones.
|
|
2038
2059
|
const rows = a.rows.map((r) => ({ name: r.name, installed: r.installed, target: r.target, tag: r.tag, state: r.state, source: r.source ?? 'npm-global', marketplace: r.marketplace ?? null }));
|
|
2039
|
-
const shadows = a.shadows.map((s) => ({ name: s.name, version: s.version, global: s.global, dir: String(s.dir).replace(
|
|
2060
|
+
const shadows = a.shadows.map((s) => ({ name: s.name, version: s.version, global: s.global, dir: String(s.dir).replace(SYSTEM_HOME, '~'), stale: !!(s.global && s.version !== s.global) }));
|
|
2040
2061
|
const by = (st) => rows.filter((r) => r.state === st).length;
|
|
2041
2062
|
const summary = { total: rows.length, behind: by('BEHIND'), broken: by('BROKEN'), ahead: by('AHEAD'), current: by('CURRENT'), unresolved: by('UNRESOLVED'), shadows: shadows.length, stale: a.stale.length };
|
|
2042
2063
|
const recommendations = buildStackRecommendations({ rows: a.rows, stale: a.stale });
|
|
@@ -2064,29 +2085,32 @@ function runNode(scriptRelPath, args) {
|
|
|
2064
2085
|
const r = spawnSync(process.execPath, [path.join(REPO, scriptRelPath), ...args], { encoding: 'utf8', timeout: 16 * 60 * 1000, cwd: REPO });
|
|
2065
2086
|
return { ok: r.status === 0, code: r.status, log: `${r.stdout || ''}${r.stderr || ''}`.trim().slice(-4000) };
|
|
2066
2087
|
}
|
|
2088
|
+
function elapsedMs(startedAt) {
|
|
2089
|
+
return Math.round((performance.now() - startedAt) * 1000) / 1000;
|
|
2090
|
+
}
|
|
2067
2091
|
|
|
2068
2092
|
// A wiring recommendation's `project` id is relative to whichever candidate root it was found under
|
|
2069
2093
|
// (issue #19) — reconstruct the absolute path by checking each root, so reconcile/undo can act on a
|
|
2070
2094
|
// project under ~/source just as well as one under ~/Code.
|
|
2071
2095
|
function resolveProjectDir(project) {
|
|
2072
|
-
for (const root of
|
|
2096
|
+
for (const root of consoleCandidateRoots()) {
|
|
2073
2097
|
const p = path.join(root, project);
|
|
2074
2098
|
if (fs.existsSync(p)) return p;
|
|
2075
2099
|
}
|
|
2076
|
-
return path.join(
|
|
2100
|
+
return path.join(CONSOLE_ROOT, 'Code', project); // last-resort fallback: the previous fixed behavior
|
|
2077
2101
|
}
|
|
2078
2102
|
// Re-derive the currently-valid recommendation set, so apply can only ever act on something STILL true.
|
|
2079
2103
|
/**
|
|
2080
2104
|
* Observe the learner's REAL state, for the health recommendations.
|
|
2081
2105
|
*
|
|
2082
|
-
* Deliberately reads the
|
|
2106
|
+
* Deliberately reads the global learner, because that is the store the capture flush
|
|
2083
2107
|
* actually writes to. Reading the project-local `.claude-flow/neural` instead is exactly the mistake
|
|
2084
2108
|
* that made the console display a dead learner (5 trajectories, last trained 6 days earlier) while
|
|
2085
2109
|
* the live one held 412 — rUv documents this fragmentation as issue #2245, "four contradictory
|
|
2086
2110
|
* sources". Until it is unified upstream we read the store that learning writes, never the corpse.
|
|
2087
2111
|
*/
|
|
2088
2112
|
function observeLearning() {
|
|
2089
|
-
const queueDir = path.join(
|
|
2113
|
+
const queueDir = path.join(SYSTEM_HOME, '.cache', 'ruvnet-brain', 'learn');
|
|
2090
2114
|
let queueDepth = 0;
|
|
2091
2115
|
try {
|
|
2092
2116
|
for (const f of fs.readdirSync(queueDir)) {
|
|
@@ -2097,10 +2121,10 @@ function observeLearning() {
|
|
|
2097
2121
|
|
|
2098
2122
|
let lastTrainSeconds = null; let trajectories = 0;
|
|
2099
2123
|
try {
|
|
2100
|
-
const r = spawnSync(path.join(
|
|
2124
|
+
const r = spawnSync(path.join(SYSTEM_HOME, '.npm-global/bin/ruflo'),
|
|
2101
2125
|
['hooks', 'intelligence', '--status'],
|
|
2102
2126
|
{
|
|
2103
|
-
cwd:
|
|
2127
|
+
cwd: SYSTEM_HOME,
|
|
2104
2128
|
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
2105
2129
|
encoding: 'utf8',
|
|
2106
2130
|
timeout: 20_000,
|
|
@@ -2167,11 +2191,15 @@ function currentValidIds(onlyId = null) {
|
|
|
2167
2191
|
return { ids, auditRows };
|
|
2168
2192
|
}
|
|
2169
2193
|
function apply(ids) {
|
|
2194
|
+
const startedAt = performance.now();
|
|
2195
|
+
const phaseMs = { revalidationMs: 0, undoJournalMs: 0, childRemedyMs: 0 };
|
|
2170
2196
|
const results = [];
|
|
2171
2197
|
for (const id of ids) {
|
|
2172
2198
|
// Re-read immediately before EACH fix. A batch can change the validity of the next item; one
|
|
2173
2199
|
// pre-flight snapshot for the whole list would let item 2 run against the world item 1 changed.
|
|
2200
|
+
const revalidationStartedAt = performance.now();
|
|
2174
2201
|
const { ids: validNow } = currentValidIds(id);
|
|
2202
|
+
phaseMs.revalidationMs += elapsedMs(revalidationStartedAt);
|
|
2175
2203
|
if (!validNow.has(id)) { results.push({ id, ok: false, skipped: true, error: 'worldMoved', log: 'Skipped — this is already resolved, or your machine changed since the page loaded. Nothing was done. Reload to see the current state.' }); continue; }
|
|
2176
2204
|
|
|
2177
2205
|
// ONE dispatch, through the registry (scripts/remedy-registry.mjs). This used to be a chain of
|
|
@@ -2213,16 +2241,22 @@ function apply(ids) {
|
|
|
2213
2241
|
// flag exists so it cannot recur here.
|
|
2214
2242
|
if (plan.exec.usesServerProject) args = [...args, '--project', process.cwd()];
|
|
2215
2243
|
if (plan.exec.needsReceipt) {
|
|
2216
|
-
const receipt = path.join(
|
|
2244
|
+
const receipt = path.join(CONSOLE_ROOT, '.cache', 'ruvnet-brain', 'undo', `${plan.key}-${stamp()}.json`);
|
|
2217
2245
|
undoSpec.receipt = receipt;
|
|
2218
2246
|
args = [...args, '--receipt', receipt];
|
|
2219
2247
|
}
|
|
2220
2248
|
|
|
2249
|
+
const journalStartedAt = performance.now();
|
|
2221
2250
|
const undoToken = journalUndo(undoSpec);
|
|
2251
|
+
phaseMs.undoJournalMs += elapsedMs(journalStartedAt);
|
|
2252
|
+
const remedyStartedAt = performance.now();
|
|
2222
2253
|
const res = runNode(plan.exec.script, args);
|
|
2254
|
+
phaseMs.childRemedyMs += elapsedMs(remedyStartedAt);
|
|
2223
2255
|
results.push({ id, ...res, undoToken });
|
|
2224
2256
|
}
|
|
2225
|
-
|
|
2257
|
+
// This receipt contains only aggregate durations, never the applied ids, paths, command output,
|
|
2258
|
+
// or undo tokens. It makes an end-user-visible delay diagnosable without exposing their machine.
|
|
2259
|
+
return { results, timings: { ...phaseMs, totalMs: elapsedMs(startedAt) } };
|
|
2226
2260
|
}
|
|
2227
2261
|
|
|
2228
2262
|
function autoEligibleIds(recommendations = []) {
|
|
@@ -2294,7 +2328,7 @@ function validateConfigPatch(values) {
|
|
|
2294
2328
|
*/
|
|
2295
2329
|
function saveConfig(values) {
|
|
2296
2330
|
try { fs.mkdirSync(CONFIG_DIR, { recursive: true }); }
|
|
2297
|
-
catch (e) { return { ok: false, log: `could not create ${CONFIG_DIR.replace(
|
|
2331
|
+
catch (e) { return { ok: false, log: `could not create ${CONFIG_DIR.replace(CONSOLE_ROOT, '~')}: ${e.message}` }; }
|
|
2298
2332
|
|
|
2299
2333
|
const { clean, rejected } = validateConfigPatch(values);
|
|
2300
2334
|
// Nothing valid to write is not a save. Saying "Saved." here would be the dead-button failure with
|
|
@@ -2356,7 +2390,7 @@ function saveConfig(values) {
|
|
|
2356
2390
|
// A successfully encrypted credential retires the legacy plaintext field on this same commit.
|
|
2357
2391
|
if (credentialChange?.ok) delete next.openrouterKey;
|
|
2358
2392
|
try { writeAtomic(CONFIG_PATH, JSON.stringify(next, null, 2) + '\n'); }
|
|
2359
|
-
catch (e) { return { ok: false, backup, log: `write failed: ${e.message}${backup ? `; your previous settings are at ${backup.replace(
|
|
2393
|
+
catch (e) { return { ok: false, backup, log: `write failed: ${e.message}${backup ? `; your previous settings are at ${backup.replace(CONSOLE_ROOT, '~')}` : ''}` }; }
|
|
2360
2394
|
try { fs.chmodSync(CONFIG_PATH, 0o600); } catch { /* best effort on non-posix */ }
|
|
2361
2395
|
|
|
2362
2396
|
// The undo token is journalled only AFTER the write succeeded. Recording an undo for a save that
|
|
@@ -2370,7 +2404,7 @@ function saveConfig(values) {
|
|
|
2370
2404
|
secretPath: credentialChange?.path ?? null,
|
|
2371
2405
|
secretExisted: credentialChange?.existed ?? null,
|
|
2372
2406
|
});
|
|
2373
|
-
return { ok: true, backup: backup ? backup.replace(
|
|
2407
|
+
return { ok: true, backup: backup ? backup.replace(CONSOLE_ROOT, '~') : null, undoToken, rejected };
|
|
2374
2408
|
});
|
|
2375
2409
|
|
|
2376
2410
|
if (held.timedOut) {
|
|
@@ -2464,7 +2498,7 @@ function undo(undoToken) {
|
|
|
2464
2498
|
try {
|
|
2465
2499
|
const bytes = fs.readFileSync(entry.backup);
|
|
2466
2500
|
held = withLock(CONFIG_PATH, () => writeAtomic(CONFIG_PATH, bytes));
|
|
2467
|
-
} catch (e) { return { ok: false, log: `restore failed: ${e.message} — your backup at ${entry.backup.replace(
|
|
2501
|
+
} catch (e) { return { ok: false, log: `restore failed: ${e.message} — your backup at ${entry.backup.replace(CONSOLE_ROOT, '~')} is intact` }; }
|
|
2468
2502
|
if (held.timedOut) return { ok: false, log: `another process is writing your settings and did not finish within ${LOCK_WAIT_MS}ms — NOTHING was restored and your backup is intact; try again` };
|
|
2469
2503
|
try { fs.chmodSync(CONFIG_PATH, 0o600); } catch { /* best effort on non-posix */ }
|
|
2470
2504
|
const effectFailures = restoreConfigEffects(entry);
|
|
@@ -2524,10 +2558,10 @@ function undo(undoToken) {
|
|
|
2524
2558
|
const base = `${path.basename(entry.db)}.rescue-`;
|
|
2525
2559
|
let baks = [];
|
|
2526
2560
|
try { baks = fs.readdirSync(dir).filter((n) => n.startsWith(base)).sort(); } catch { /* dir gone */ }
|
|
2527
|
-
if (!baks.length) return { ok: false, log: `no pre-repair backup found next to ${entry.db.replace(
|
|
2561
|
+
if (!baks.length) return { ok: false, log: `no pre-repair backup found next to ${entry.db.replace(CONSOLE_ROOT, '~')} — nothing was restored` };
|
|
2528
2562
|
const from = path.join(dir, baks[baks.length - 1]);
|
|
2529
2563
|
try { fs.copyFileSync(from, entry.db); }
|
|
2530
|
-
catch (e) { return { ok: false, log: `could not restore ${from.replace(
|
|
2564
|
+
catch (e) { return { ok: false, log: `could not restore ${from.replace(CONSOLE_ROOT, '~')}: ${e.message}` }; }
|
|
2531
2565
|
markUndoConsumed(undoToken);
|
|
2532
2566
|
return { ok: true, log: `restored your memory store from the snapshot taken before the repair (${baks[baks.length - 1]})` };
|
|
2533
2567
|
}
|
|
@@ -2801,7 +2835,7 @@ function startServer({ port = Number(process.env.CONSOLE_PORT) || 7411, open = f
|
|
|
2801
2835
|
// precise moment a first-time user is deciding whether this thing works. The child does that
|
|
2802
2836
|
// scan now (see --refresh-cache), and gatherActivity reports `warming` until it lands.
|
|
2803
2837
|
if (open) openBrowser(url);
|
|
2804
|
-
// Browser acceptance pre-warms a disposable
|
|
2838
|
+
// Browser acceptance pre-warms a disposable fixture root and disables only this redundant second scan.
|
|
2805
2839
|
// The production default is unchanged: every ordinary console start refreshes in the background.
|
|
2806
2840
|
if (process.env.RUVNET_CONSOLE_DISABLE_BACKGROUND_REFRESH !== '1') kickRefresh({ force: true });
|
|
2807
2841
|
});
|
|
@@ -33,9 +33,9 @@ export function verdictOf(results) {
|
|
|
33
33
|
return results.reduce((worst, r) => (RANK.indexOf(r.state) < RANK.indexOf(worst) ? r.state : worst), 'PASS');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function candidateLineage() {
|
|
36
|
+
export function candidateLineage(root = ROOT) {
|
|
37
37
|
const git = (args) => {
|
|
38
|
-
const r = spawnSync('git', args, { cwd:
|
|
38
|
+
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
39
39
|
return r.status === 0 ? r.stdout.trim() : '';
|
|
40
40
|
};
|
|
41
41
|
return {
|
|
@@ -49,11 +49,8 @@ export function verdictWithLineage(results, lineage) {
|
|
|
49
49
|
return lineage?.dirty ? 'FAIL' : verdictOf(results);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const exists = (rel) => fs.existsSync(path.join(ROOT, rel));
|
|
53
|
-
const read = (rel) => { try { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); } catch { return null; } };
|
|
54
|
-
|
|
55
52
|
/** Run a command; UNKNOWN (not FAIL) when the runner itself could not execute. */
|
|
56
|
-
function
|
|
53
|
+
function runAt(root, cmd, args, timeoutMs = 120_000, env = process.env) {
|
|
57
54
|
// npm installs `npm.cmd` / `npx.cmd` command shims on Windows. Direct spawn does not resolve the
|
|
58
55
|
// extension through PATHEXT, and .cmd files require cmd.exe, so a healthy detector otherwise
|
|
59
56
|
// reads UNKNOWN with ENOENT. Invoke the command interpreter explicitly rather than `shell:true`;
|
|
@@ -62,7 +59,7 @@ function run(cmd, args, timeoutMs = 120_000, env = process.env) {
|
|
|
62
59
|
const executable = windowsCommandShim ? (process.env.ComSpec || 'cmd.exe') : cmd;
|
|
63
60
|
const spawnArgs = windowsCommandShim ? ['/d', '/c', `${cmd}.cmd`, ...args] : args;
|
|
64
61
|
const r = spawnSync(executable, spawnArgs, {
|
|
65
|
-
cwd:
|
|
62
|
+
cwd: root,
|
|
66
63
|
encoding: 'utf8',
|
|
67
64
|
timeout: timeoutMs,
|
|
68
65
|
env,
|
|
@@ -71,6 +68,18 @@ function run(cmd, args, timeoutMs = 120_000, env = process.env) {
|
|
|
71
68
|
return { state: r.status === 0 ? 'PASS' : 'FAIL', why: r.status === 0 ? 'exit 0' : `exit ${r.status}`, out: r.stdout };
|
|
72
69
|
}
|
|
73
70
|
|
|
71
|
+
/** An explicit detector root makes destructive mutants isolated and concurrency-safe. */
|
|
72
|
+
function detectorScope({ root = ROOT, runCommand = null } = {}) {
|
|
73
|
+
return {
|
|
74
|
+
root,
|
|
75
|
+
exists: (relative) => fs.existsSync(path.join(root, relative)),
|
|
76
|
+
read: (relative) => {
|
|
77
|
+
try { return fs.readFileSync(path.join(root, relative), 'utf8'); } catch { return null; }
|
|
78
|
+
},
|
|
79
|
+
run: runCommand || ((cmd, args, timeoutMs, env) => runAt(root, cmd, args, timeoutMs, env)),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
// ─── the eight invariants ────────────────────────────────────────────────────────────────────
|
|
75
84
|
// Each names the dimension it gates and the dated failure it descends from. A detector with no
|
|
76
85
|
// incident behind it is a checkbox; every one of these was bought with a real defect.
|
|
@@ -80,7 +89,8 @@ export const INVARIANTS = [
|
|
|
80
89
|
name: 'INSTALL-FAILS-LOUD',
|
|
81
90
|
dimension: 'D8',
|
|
82
91
|
incident: 'verifyInstall() warned and continued; a stranger scored 18/100 on an install that could not fail',
|
|
83
|
-
detect() {
|
|
92
|
+
detect(options) {
|
|
93
|
+
const { exists, read } = detectorScope(options);
|
|
84
94
|
if (!exists('.github/workflows/stranger-matrix.yml')) return { state: 'FAIL', why: 'no stranger matrix' };
|
|
85
95
|
const inst = read('bin/install.mjs');
|
|
86
96
|
if (inst === null) return { state: 'UNKNOWN', why: 'bin/install.mjs unreadable' };
|
|
@@ -95,7 +105,8 @@ export const INVARIANTS = [
|
|
|
95
105
|
name: 'INTERFACE-CORPUS',
|
|
96
106
|
dimension: 'D7',
|
|
97
107
|
incident: 'shell-parsing defects recurred across issues #12/#13/#41/#44, then three stale ADRs reached main because document currency was outside every release gate',
|
|
98
|
-
detect() {
|
|
108
|
+
detect(options) {
|
|
109
|
+
const { exists, run } = detectorScope(options);
|
|
99
110
|
if (!exists('tests/regression/interface-gate-corpus.test.mjs')) return { state: 'FAIL', why: 'no incident corpus' };
|
|
100
111
|
if (!exists('scripts/doc-currency.mjs')) return { state: 'FAIL', why: 'no document-currency gate' };
|
|
101
112
|
const corpus = run('npx', ['vitest', 'run', 'tests/regression/interface-gate-corpus.test.mjs', '--reporter=dot']);
|
|
@@ -110,23 +121,29 @@ export const INVARIANTS = [
|
|
|
110
121
|
name: 'LATENCY-DECISION-LANE',
|
|
111
122
|
dimension: 'D6',
|
|
112
123
|
incident: 'latency breaches only warned; timing regressions did not block shipping',
|
|
113
|
-
detect
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
detect(options) {
|
|
125
|
+
const { exists, run } = detectorScope(options);
|
|
126
|
+
return exists('scripts/qe/card-lane-gate.mjs')
|
|
127
|
+
? run('node', ['scripts/qe/card-lane-gate.mjs'])
|
|
128
|
+
: { state: 'FAIL', why: 'no decision-lane gate' };
|
|
129
|
+
},
|
|
116
130
|
},
|
|
117
131
|
{
|
|
118
132
|
name: 'COEXIST-BYTE-EQUAL',
|
|
119
133
|
dimension: 'D5',
|
|
120
134
|
incident: 'the merged-registry lint found 63 findings across 42 registrations; the prior suite saw 15',
|
|
121
|
-
detect
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
detect(options) {
|
|
136
|
+
const { exists, run } = detectorScope(options);
|
|
137
|
+
return exists('tests/mesh/coexistence.test.mjs')
|
|
138
|
+
? run('npx', ['vitest', 'run', 'tests/mesh', '--reporter=dot'])
|
|
139
|
+
: { state: 'FAIL', why: 'no coexistence suite' };
|
|
140
|
+
},
|
|
124
141
|
},
|
|
125
142
|
{
|
|
126
143
|
name: 'LEARNING-REPLAY',
|
|
127
144
|
dimension: 'D4',
|
|
128
145
|
incident: 'L4 asserted the hook\'s own prose contained words — it proved the brain SPOKE, never that anything LISTENED',
|
|
129
|
-
async detect() {
|
|
146
|
+
async detect(options = {}) {
|
|
130
147
|
// DELEGATED to scripts/learning-replay.mjs's own checkArtifact(), not reimplemented here.
|
|
131
148
|
//
|
|
132
149
|
// This detector originally required `artifact.sha === HEAD`, which is WRONG in a way worth
|
|
@@ -141,7 +158,9 @@ export const INVARIANTS = [
|
|
|
141
158
|
try { mod = await import(new URL('./learning-replay.mjs', import.meta.url).href); }
|
|
142
159
|
catch { return { state: 'UNKNOWN', why: 'counterfactual replay harness absent' }; }
|
|
143
160
|
let r;
|
|
144
|
-
try {
|
|
161
|
+
try {
|
|
162
|
+
r = options.checkReplay ? options.checkReplay() : mod.checkPortfolio({ repo: options.root || ROOT });
|
|
163
|
+
} catch (e) { return { state: 'UNKNOWN', why: `replay portfolio unreadable: ${e.message}` }; }
|
|
145
164
|
// `.status` is the field checkArtifact() actually returns — read, not assumed. Reaching for
|
|
146
165
|
// `.verdict` (the name used INSIDE the nested artifact) silently yielded undefined and made a
|
|
147
166
|
// real PASS read UNKNOWN: the delegation "worked" while reporting the opposite of the truth.
|
|
@@ -157,7 +176,8 @@ export const INVARIANTS = [
|
|
|
157
176
|
name: 'SIGNAL-WATCH-FIRES',
|
|
158
177
|
dimension: 'D3',
|
|
159
178
|
incident: 'the OWNER had to report that CI was red — every local surface stayed calm',
|
|
160
|
-
detect() {
|
|
179
|
+
detect(options) {
|
|
180
|
+
const { exists, read, run } = detectorScope(options);
|
|
161
181
|
if (!exists('scripts/signal-watch.mjs')) return { state: 'FAIL', why: 'no signal watcher' };
|
|
162
182
|
const raw = read('plugin/hooks/hooks.json');
|
|
163
183
|
if (raw === null) return { state: 'UNKNOWN', why: 'plugin/hooks/hooks.json unreadable' };
|
|
@@ -207,15 +227,19 @@ export const INVARIANTS = [
|
|
|
207
227
|
name: 'SCENARIOS-CURRENT',
|
|
208
228
|
dimension: 'D2',
|
|
209
229
|
incident: 'ADR-053 specified a mental-model scenario list; it was simply absent',
|
|
210
|
-
detect
|
|
211
|
-
|
|
212
|
-
|
|
230
|
+
detect(options) {
|
|
231
|
+
const { exists, run } = detectorScope(options);
|
|
232
|
+
return exists('tests/experience/report.mjs')
|
|
233
|
+
? run('node', ['tests/experience/report.mjs'])
|
|
234
|
+
: { state: 'FAIL', why: 'no scenario report' };
|
|
235
|
+
},
|
|
213
236
|
},
|
|
214
237
|
{
|
|
215
238
|
name: 'GUARANTEE-RUNS',
|
|
216
239
|
dimension: 'D1',
|
|
217
240
|
incident: 'the product guarantee skipped on every CI runner; REQUIRE_BRAIN=1 was set nowhere in the repo',
|
|
218
|
-
detect() {
|
|
241
|
+
detect(options) {
|
|
242
|
+
const { read, run } = detectorScope(options);
|
|
219
243
|
const ci = read('.github/workflows/ci.yml');
|
|
220
244
|
if (ci === null) return { state: 'UNKNOWN', why: 'ci.yml unreadable' };
|
|
221
245
|
if (!/REQUIRE_BRAIN/.test(ci)) {
|
|
@@ -247,12 +271,12 @@ export function headSha() {
|
|
|
247
271
|
/** Strings a non-PASS verdict mechanically bans from release surfaces (ADR-058). */
|
|
248
272
|
export const BANNED_WHEN_DEGRADED = ['healthy', 'proven', 'all pass'];
|
|
249
273
|
|
|
250
|
-
export async function evaluate(invariants = INVARIANTS) {
|
|
251
|
-
const lineage = candidateLineage();
|
|
274
|
+
export async function evaluate(invariants = INVARIANTS, options = {}) {
|
|
275
|
+
const lineage = candidateLineage(options.root || ROOT);
|
|
252
276
|
const sha = lineage.sha;
|
|
253
277
|
const results = [];
|
|
254
278
|
for (const inv of invariants) {
|
|
255
|
-
const r = await inv.detect();
|
|
279
|
+
const r = await inv.detect(options);
|
|
256
280
|
results.push({ name: inv.name, dimension: inv.dimension, state: r.state, why: r.why, sha });
|
|
257
281
|
}
|
|
258
282
|
return { sha, lineage, results, verdict: verdictWithLineage(results, lineage) };
|