ruvnet-brain 4.3.20 → 4.3.25
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 +5 -5
- package/bin/install.mjs +383 -78
- package/console/app.js +141 -9
- package/console/index.html +51 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/host-shell-boundary.mjs +43 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/scripts/update-apply.mjs +2 -32
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +319 -27
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +23 -6
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
|
@@ -39,6 +39,7 @@ import os from 'node:os';
|
|
|
39
39
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
40
40
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
41
|
import { NIGHTLY_LABEL, schedulerStatus } from '../plugin/scripts/nightly-scheduler.mjs';
|
|
42
|
+
import { oldestIncompleteProgress } from '../kb/shard-progress.mjs';
|
|
42
43
|
|
|
43
44
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
44
45
|
const REGISTRY = process.env.WATCHDOG_REGISTRY || path.join(ROOT, 'config', 'scheduled-jobs.json');
|
|
@@ -51,6 +52,28 @@ export const MISSING = 'MISSING';
|
|
|
51
52
|
export const NEVER_RAN = 'NEVER-RAN';
|
|
52
53
|
export const STALE = 'STALE';
|
|
53
54
|
export const FAILING = 'FAILING';
|
|
55
|
+
export const STALLED = 'STALLED';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Per-shard progress evidence for a job that declares `progressGlob` + `stallMinutes` in the
|
|
59
|
+
* registry (see kb/shard-progress.mjs, written by kb/forge-big.mjs's `embed`/`shard-all` modes).
|
|
60
|
+
*
|
|
61
|
+
* WHY THIS EXISTS (2026-09-11): a live wrapper pid proves the WRAPPER survived a run; it proves
|
|
62
|
+
* NOTHING about whether the work inside it is still moving — an 8-shard gists embed once sat at 0%
|
|
63
|
+
* CPU for six hours with `judge()`'s existing pid-liveness check reporting it OK the entire time
|
|
64
|
+
* (the pid genuinely was alive; it just was not doing anything). `job.stallMinutes` is deliberately
|
|
65
|
+
* independent of `job.maxAgeHours`/the 6h long-run branch below — a job stuck at 0% progress for 20
|
|
66
|
+
* minutes must be flagged well before its overall schedule budget or the long-run grace period would
|
|
67
|
+
* ever trip.
|
|
68
|
+
*/
|
|
69
|
+
export function readJobProgress(job, { root = ROOT } = {}) {
|
|
70
|
+
if (!job.progressGlob) return null;
|
|
71
|
+
const full = path.join(root, job.progressGlob);
|
|
72
|
+
const dir = path.dirname(full);
|
|
73
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
74
|
+
const pattern = new RegExp(`^${path.basename(full).split('*').map(escapeRegExp).join('.*')}$`);
|
|
75
|
+
return oldestIncompleteProgress(dir, pattern);
|
|
76
|
+
}
|
|
54
77
|
|
|
55
78
|
/** Labels currently loaded in launchd. A job that isn't here CANNOT fire, whatever its plist says. */
|
|
56
79
|
export function loadedLabels(run = () => spawnSync('launchctl', ['list'], { encoding: 'utf8' }).stdout || '') {
|
|
@@ -67,7 +90,7 @@ export function loadedLabels(run = () => spawnSync('launchctl', ['list'], { enco
|
|
|
67
90
|
* Judge one job from its receipt. `hb === null` means NO RECEIPT EXISTS — which is NEVER-RAN, never OK.
|
|
68
91
|
* This single line is the whole lesson of 2026-07-13: absence of evidence is not evidence of health.
|
|
69
92
|
*/
|
|
70
|
-
export function judge(job, hb, loaded, now) {
|
|
93
|
+
export function judge(job, hb, loaded, now, { root = ROOT } = {}) {
|
|
71
94
|
if (!loaded) {
|
|
72
95
|
return { state: MISSING, detail: 'declared in the registry but NOT LOADED in launchd — it can never fire' };
|
|
73
96
|
}
|
|
@@ -80,6 +103,20 @@ export function judge(job, hb, loaded, now) {
|
|
|
80
103
|
}
|
|
81
104
|
const ageHours = (now - stamp) / HOUR;
|
|
82
105
|
|
|
106
|
+
// STALLED: checked BEFORE the long-run pid-liveness branch below, and independent of it — a live
|
|
107
|
+
// pid is not proof of progress. Only applies when the job declares a stall budget; absence of a
|
|
108
|
+
// progress file is never treated as a stall (the job may not have reached embedding yet).
|
|
109
|
+
if (hb.state === 'running' && job.stallMinutes) {
|
|
110
|
+
const progress = readJobProgress(job, { root });
|
|
111
|
+
if (progress) {
|
|
112
|
+
const staleMinutes = (now - progress.updatedAt) / 60_000;
|
|
113
|
+
if (staleMinutes > job.stallMinutes) {
|
|
114
|
+
return { state: STALLED, ageHours,
|
|
115
|
+
detail: `${progress.detail} has not advanced in ${staleMinutes.toFixed(0)}m — its stall budget is ${job.stallMinutes}m. The wrapper pid may be alive; the work is not.` };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
// Started and never finished? DERIVE it from process liveness, not wall-clock alone (2026-07-19):
|
|
84
121
|
// a full corpus rebuild legitimately runs 12h+ (five changed repos = a long day), and the old
|
|
85
122
|
// ">6h running ⇒ FAILING" rule false-alarmed on exactly that — a 13.5h nightly whose worker was
|
|
@@ -110,8 +147,12 @@ export function judge(job, hb, loaded, now) {
|
|
|
110
147
|
if (hb.state === 'skipped') {
|
|
111
148
|
return { state: NEVER_RAN, ageHours, detail: 'only a lock-skip receipt exists — no real run has ever recorded a result' };
|
|
112
149
|
}
|
|
113
|
-
if (hb.state === 'failed' || (typeof hb.exit_code === 'number' && hb.exit_code !== 0)) {
|
|
114
|
-
|
|
150
|
+
if (hb.state === 'failed' || hb.state === 'killed' || (typeof hb.exit_code === 'number' && hb.exit_code !== 0)) {
|
|
151
|
+
// job-heartbeat.sh distinguishes "failed" (plain non-zero exit) from "killed" (died to a
|
|
152
|
+
// signal — the wrapper's own trap, or a direct kill of the child; POSIX 128+signal) — surface
|
|
153
|
+
// which one it was rather than collapsing both into one undifferentiated exit code.
|
|
154
|
+
const how = hb.state === 'killed' ? `KILLED by signal ${hb.signal ?? '?'}` : `FAILED with exit ${hb.exit_code}`;
|
|
155
|
+
return { state: FAILING, ageHours, detail: `last run ${how} (${ageHours.toFixed(1)}h ago)` };
|
|
115
156
|
}
|
|
116
157
|
if (hb.state === 'running') {
|
|
117
158
|
return { state: OK, ageHours, detail: `running right now (started ${ageHours.toFixed(1)}h ago)` };
|
|
@@ -153,7 +194,7 @@ export function checkAll(now, { registry = REGISTRY, loaded = loadedLabels(), hb
|
|
|
153
194
|
const { jobs } = JSON.parse(fs.readFileSync(registry, 'utf8'));
|
|
154
195
|
return jobs.map((job) => {
|
|
155
196
|
const hb = readHeartbeat(job.label, hbDir) || readLegacyLog(job, root);
|
|
156
|
-
const verdict = judge(job, hb, loaded.has(job.label), now);
|
|
197
|
+
const verdict = judge(job, hb, loaded.has(job.label), now, { root });
|
|
157
198
|
if (hb?._tier2 && verdict.state === OK) verdict.detail += ' (via its log — no receipt yet; the next run writes one)';
|
|
158
199
|
return { ...job, ...verdict };
|
|
159
200
|
});
|
|
@@ -162,6 +203,22 @@ export function checkAll(now, { registry = REGISTRY, loaded = loadedLabels(), hb
|
|
|
162
203
|
export function productSchedulerVerdict(status) {
|
|
163
204
|
const base = { label: NIGHTLY_LABEL, what: 'Refreshes the installed Brain corpus and converges every detected host',
|
|
164
205
|
schedule: 'daily 03:47', maxAgeHours: 30, required: true };
|
|
206
|
+
// A 'degraded' adapter whose only defect is a NON-ZERO LAST EXIT is a job that is loaded, verified
|
|
207
|
+
// and FIRED — the exact opposite of MISSING ("not loaded — it can never fire", top of this file).
|
|
208
|
+
// Its truth is the receipt. Measured 2026-09-12 against two FAILED receipts: this projected as
|
|
209
|
+
// MISSING with the detail "LaunchAgent is loaded and runner digest verified, but last exited 1" —
|
|
210
|
+
// a contradiction in one line, and the receipt's reason never surfaced.
|
|
211
|
+
if (status?.state === 'degraded' && Number.isInteger(status.lastExitCode) && status.lastExitCode !== 0) {
|
|
212
|
+
const run = status.runHealth || null;
|
|
213
|
+
if (run?.state === 'stale') return { ...base, state: STALE, detail: run.evidence };
|
|
214
|
+
if (!run || run.state === 'never-ran') {
|
|
215
|
+
return { ...base, state: FAILING, detail: `runner last exited ${status.lastExitCode} and left no receipt — it fired and died before recording a run` };
|
|
216
|
+
}
|
|
217
|
+
if (run.state === 'ok' || run.state === 'running') {
|
|
218
|
+
return { ...base, state: FAILING, detail: `runner last exited ${status.lastExitCode} although its latest receipt reads ${run.state} — the run after that receipt died without recording one (${run.evidence})` };
|
|
219
|
+
}
|
|
220
|
+
return { ...base, state: FAILING, detail: run.evidence };
|
|
221
|
+
}
|
|
165
222
|
if (status?.state !== 'on') return { ...base, state: MISSING, detail: status?.evidence || 'scheduler is not registered' };
|
|
166
223
|
const run = status.runHealth || { state: 'never-ran', evidence: 'No nightly refresh receipt exists yet.' };
|
|
167
224
|
const mapped = run.state === 'ok' || run.state === 'running' ? OK
|
|
@@ -33,6 +33,7 @@ import { planFor } from './remedy-registry.mjs';
|
|
|
33
33
|
import { auditAll as capabilityAuditAll } from './capability-registry.mjs';
|
|
34
34
|
import { getVersion } from './version.mjs';
|
|
35
35
|
import { consoleRuntimeDigest } from './console-runtime-identity.mjs';
|
|
36
|
+
import { isCorpusReleaseTag } from '../kb/corpus-release-identity.mjs';
|
|
36
37
|
// L5 (ADR-028): the audit is the one place that observes live capability state, so it is where an
|
|
37
38
|
// OFFERED-then-now-`on` transition becomes an APPLIED — the numerator of the precision metric that
|
|
38
39
|
// tells the owner whether advocacy is landing or nagging. Both are pure reads/appends and never throw.
|
|
@@ -422,6 +423,27 @@ function wiringSurvey() {
|
|
|
422
423
|
if (mech) sites.push({ scope: 'project', project: projName, file: '.mcp.json', event: 'MCP', matcher: name, spec: full.slice(0, 160), mechanism: mech });
|
|
423
424
|
}
|
|
424
425
|
}
|
|
426
|
+
// THE MACHINE-WIDE FILE (console audit 2026-09-11). Plugins are switched on in ~/.claude/settings.json
|
|
427
|
+
// and nowhere else, and this survey never read that file — so the card said `plugin: 0` on a machine
|
|
428
|
+
// with 14 enabled, ruvnet-brain among them. Each enabled plugin is one global PLUGIN site. The file's
|
|
429
|
+
// own hooks go through the same classifier the project scan uses, so a machine-wide npx or
|
|
430
|
+
// global-binary hook is counted in the same words as a project one.
|
|
431
|
+
const machine = readJSON(path.join(CONSOLE_ROOT, '.claude', 'settings.json'));
|
|
432
|
+
if (machine) {
|
|
433
|
+
for (const [event, groups] of Object.entries(machine.hooks || {})) {
|
|
434
|
+
const list = Array.isArray(groups) ? groups : [groups];
|
|
435
|
+
for (const g of list) {
|
|
436
|
+
const hookArr = Array.isArray(g?.hooks) ? g.hooks : (g?.command ? [g] : []);
|
|
437
|
+
for (const h of hookArr) {
|
|
438
|
+
const mech = classifyCommand(h?.command);
|
|
439
|
+
if (mech) sites.push({ scope: 'global', file: '~/.claude/settings.json', event, matcher: g?.matcher ?? '*', spec: String(h.command).slice(0, 160), mechanism: mech });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const [name, on] of Object.entries(machine.enabledPlugins || {})) {
|
|
444
|
+
if (on === true) sites.push({ scope: 'global', file: '~/.claude/settings.json', event: 'plugin', matcher: name, spec: name, mechanism: 'PLUGIN' });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
425
447
|
return { sites, summary: summarizeWiring(sites) };
|
|
426
448
|
}
|
|
427
449
|
|
|
@@ -444,25 +466,41 @@ function sessionHookExists() {
|
|
|
444
466
|
//
|
|
445
467
|
// So the probe stops guessing and asks the filesystem: consider both known names, and use whichever
|
|
446
468
|
// actually holds rows. That is correct on their machine and on mine without knowing why they differ.
|
|
469
|
+
//
|
|
470
|
+
// THE SECOND HALF OF THAT LESSON (console audit 2026-09-11). "Whichever holds rows" was implemented
|
|
471
|
+
// as "whichever is LARGER", and on a machine where BOTH hold rows that picked the wrong one: the
|
|
472
|
+
// card scored ruvnet-brain on `.swarm/agentdb-memory.db` — 15,824 rows, the MCP coordination store
|
|
473
|
+
// the global CLAUDE.md warns is a DIFFERENT container — and rendered "15824 entries, 100% embedded"
|
|
474
|
+
// while the canonical `.swarm/memory.db`, the file `ruflo memory store` writes, held 5. The #127
|
|
475
|
+
// reporter's cases (canonical absent, or present but empty) still resolve to the coordination store;
|
|
476
|
+
// a canonical store that holds rows is now always the one scored, and BOTH files are reported so the
|
|
477
|
+
// page can say which it scored and which it did not.
|
|
447
478
|
const MEMORY_DB_NAMES = ['memory.db', 'agentdb-memory.db'];
|
|
479
|
+
// Row count that never throws and never hangs on a live store: null means "unreadable this instant".
|
|
480
|
+
function memoryRows(file) {
|
|
481
|
+
if (!fs.existsSync(file)) return null;
|
|
482
|
+
const r = robustRead(file, 'SELECT count(*) FROM memory_entries;');
|
|
483
|
+
return r.ok ? (r.value === null ? 0 : parseInt(r.value, 10)) : null;
|
|
484
|
+
}
|
|
485
|
+
export function memoryStores(projectDir) {
|
|
486
|
+
const [canonicalPath, coordinationPath] = MEMORY_DB_NAMES.map((name) => path.join(projectDir, '.swarm', name));
|
|
487
|
+
const canonical = { path: canonicalPath, exists: fs.existsSync(canonicalPath), rows: memoryRows(canonicalPath) };
|
|
488
|
+
const coordination = { path: coordinationPath, exists: fs.existsSync(coordinationPath), rows: memoryRows(coordinationPath) };
|
|
489
|
+
// A canonical store that holds rows — or is busy being written this instant (rows null) — is the
|
|
490
|
+
// one scored. Only an absent or EMPTY canonical store hands the score to the coordination file.
|
|
491
|
+
const scored = canonical.exists && (canonical.rows === null || canonical.rows > 0) ? canonical.path
|
|
492
|
+
: coordination.exists ? coordination.path
|
|
493
|
+
: canonical.path; // canonical name for the "absent" message
|
|
494
|
+
return { canonical, coordination, scored };
|
|
495
|
+
}
|
|
448
496
|
export function resolveMemoryDb(projectDir) {
|
|
449
|
-
|
|
450
|
-
.map((name) => path.join(projectDir, '.swarm', name))
|
|
451
|
-
.filter((file) => fs.existsSync(file));
|
|
452
|
-
if (!candidates.length) return path.join(projectDir, '.swarm/memory.db'); // canonical name for the "absent" message
|
|
453
|
-
if (candidates.length === 1) return candidates[0];
|
|
454
|
-
// Both present: prefer the one with real content. Size is a proxy for rows that costs no query and
|
|
455
|
-
// cannot throw on a locked or WAL-mode database — this runs inside a UI probe that must never hang.
|
|
456
|
-
return candidates.sort((a, b) => {
|
|
457
|
-
const sa = (() => { try { return fs.statSync(a).size; } catch { return 0; } })();
|
|
458
|
-
const sb = (() => { try { return fs.statSync(b).size; } catch { return 0; } })();
|
|
459
|
-
return sb - sa;
|
|
460
|
-
})[0];
|
|
497
|
+
return memoryStores(projectDir).scored;
|
|
461
498
|
}
|
|
462
499
|
|
|
463
500
|
function probeMemory(projectDir, { now = Date.now() } = {}) {
|
|
464
|
-
const
|
|
465
|
-
const
|
|
501
|
+
const stores = memoryStores(projectDir);
|
|
502
|
+
const db = stores.scored;
|
|
503
|
+
const probes = { stores };
|
|
466
504
|
// compaction survival + session surfacing are filesystem facts, always checkable
|
|
467
505
|
const snapshot = inspectSessionSnapshots(projectDir, { now });
|
|
468
506
|
const snapshotDetail = snapshot.kind === 'canonical'
|
|
@@ -503,8 +541,10 @@ function probeMemory(projectDir, { now = Date.now() } = {}) {
|
|
|
503
541
|
const liveNote = integ.mode === 'immutable=1' ? ' and in active use' : '';
|
|
504
542
|
if (integrity !== 'ok') probes.liveness = { status: 'fail', detail: `store is corrupt (integrity_check: ${integrity})` };
|
|
505
543
|
else if (total === null) probes.liveness = { status: 'notTested', detail: 'store opened but counts were unavailable this instant' };
|
|
506
|
-
|
|
507
|
-
|
|
544
|
+
// The file is NAMED in the sentence, so "15824 entries" can never again be read as the canonical
|
|
545
|
+
// store's count when it was the coordination store's.
|
|
546
|
+
else if (total > 0) probes.liveness = { status: 'ok', detail: `${path.basename(db)} is live${liveNote}, integrity ok, ${total} entries${embedded != null && total ? `, ${Math.round((embedded / total) * 100)}% embedded` : ''} (read-only)` };
|
|
547
|
+
else probes.liveness = { status: 'warn', detail: `${path.basename(db)} exists but is empty` };
|
|
508
548
|
|
|
509
549
|
const cp = robustRead(db, "SELECT max(updated_at) FROM memory_entries WHERE key LIKE 'project-state-current%';");
|
|
510
550
|
if (cp.ok && cp.value) {
|
|
@@ -542,12 +582,20 @@ function gatherMemory(cwd, { fleet = true } = {}) {
|
|
|
542
582
|
const scope = projectDirectory({ cwd });
|
|
543
583
|
const project = fs.existsSync(path.join(scope, '.swarm/memory.db')) ? scope : REPO;
|
|
544
584
|
const projName = project.replace(CONSOLE_ROOT + '/Code/', '').replace(CONSOLE_ROOT + '/', '~/');
|
|
545
|
-
const
|
|
585
|
+
const probes = probeMemory(project);
|
|
586
|
+
const health = scoreMemoryHealth({ project: projName, probes });
|
|
587
|
+
// Which file the score is about, and which file it deliberately is not — both with row counts.
|
|
588
|
+
const short = (p) => String(p).replace(CONSOLE_ROOT, '~');
|
|
589
|
+
health.stores = probes.stores ? {
|
|
590
|
+
scored: short(probes.stores.scored),
|
|
591
|
+
canonical: { ...probes.stores.canonical, path: short(probes.stores.canonical.path) },
|
|
592
|
+
coordination: { ...probes.stores.coordination, path: short(probes.stores.coordination.path) },
|
|
593
|
+
} : null;
|
|
546
594
|
return { fleet: fleet ? scanFleet() : null, health };
|
|
547
595
|
}
|
|
548
596
|
|
|
549
597
|
// ── Savings ledger (receipts only) ────────────────────────────────────────────────────────────────
|
|
550
|
-
function gatherSavings() {
|
|
598
|
+
function gatherSavings({ repo = REPO } = {}) {
|
|
551
599
|
// Primary source is the real routing-receipts ledger written by scripts/route-cheap.mjs.
|
|
552
600
|
const files = [
|
|
553
601
|
path.join(SYSTEM_HOME, '.claude/metaharness/routing-receipts.jsonl'),
|
|
@@ -555,7 +603,9 @@ function gatherSavings() {
|
|
|
555
603
|
// Canonical user-level ledger (issue #36 — the hooks no longer scatter per-CWD copies).
|
|
556
604
|
path.join(SYSTEM_HOME, '.cache/ruvnet-brain/token-ledger.jsonl'),
|
|
557
605
|
// Legacy location, still read so an existing user's history is not orphaned by the move.
|
|
558
|
-
|
|
606
|
+
// `repo` is injectable because on a developer checkout this file is REAL history (2 rows from
|
|
607
|
+
// 2026-07-14 on the owner's machine) and HOME isolation alone cannot keep it out of a fixture.
|
|
608
|
+
path.join(repo, 'plugin/scripts/.ruvnet-brain/token-ledger.jsonl'),
|
|
559
609
|
];
|
|
560
610
|
const receipts = [];
|
|
561
611
|
let baselineUsd = 0;
|
|
@@ -591,10 +641,16 @@ function gatherSavings() {
|
|
|
591
641
|
}
|
|
592
642
|
}
|
|
593
643
|
const usdSaved = +receipts.reduce((a, r) => a + (r.measuredUsd || 0), 0).toFixed(4);
|
|
644
|
+
const timed = receipts.filter((r) => r.measuredMs !== null);
|
|
594
645
|
const totals = receipts.length ? {
|
|
595
646
|
count: receipts.length,
|
|
596
647
|
usdSaved,
|
|
648
|
+
// SIGNED, and it stays signed. A net-slower ledger sums negative and is REPORTED negative — the
|
|
649
|
+
// page once turned that into a dash (console audit 2026-09-11: −46,737 ms across 25 timed
|
|
650
|
+
// routes, 23 of them slower than baseline, rendered as "—"). A loss is a finding about the
|
|
651
|
+
// router; `timedCount` says how many routes the number is even about.
|
|
597
652
|
msSaved: receipts.reduce((a, r) => a + (r.measuredMs || 0), 0),
|
|
653
|
+
timedCount: timed.length,
|
|
598
654
|
baselineUsd: +baselineUsd.toFixed(4),
|
|
599
655
|
pctSaved: baselineUsd > 0 ? Math.round((usdSaved / baselineUsd) * 100) : null,
|
|
600
656
|
} : null;
|
|
@@ -643,11 +699,42 @@ function unsupportedConfigControls() {
|
|
|
643
699
|
* off, and the Settings form shows the shipped default beside it as a recommendation rather than as
|
|
644
700
|
* a fact about their machine.
|
|
645
701
|
*/
|
|
702
|
+
/**
|
|
703
|
+
* NIGHTLY REFRESH IS THREE FACTS, NOT ONE BOOLEAN (console audit 2026-09-11). The toggle rendered
|
|
704
|
+
* `nightly: true` because the LaunchAgent was loaded, while ~/.claude/ruvnet-brain/config.json said
|
|
705
|
+
* `nightly: false` and the job had never completed a run. Three independently measured things —
|
|
706
|
+
* the CHOICE the user saved (config.json), the ENFORCEMENT the scheduler adapter reports (loaded,
|
|
707
|
+
* off, degraded), and the OUTCOME of the last completed run (its receipt) — had been collapsed into
|
|
708
|
+
* the one the user did not set. A single boolean is emitted only when choice and enforcement agree;
|
|
709
|
+
* otherwise the field is null (never chosen, or in disagreement) and `facts` carries all three so
|
|
710
|
+
* the page can print them side by side. Pure, so the rule is testable without a scheduler.
|
|
711
|
+
*/
|
|
712
|
+
export function reconcileNightly({ choice, schedule }) {
|
|
713
|
+
const enforcement = schedule?.state ?? 'unknown';
|
|
714
|
+
const run = schedule?.runHealth ?? null;
|
|
715
|
+
const enforced = enforcement === 'on' ? true : enforcement === 'off' ? false : null;
|
|
716
|
+
const chosen = choice === true || choice === false ? choice : null;
|
|
717
|
+
const agree = chosen === null ? null : (enforced !== null && chosen === enforced);
|
|
718
|
+
return {
|
|
719
|
+
value: agree === true ? chosen : null,
|
|
720
|
+
facts: {
|
|
721
|
+
choice: chosen,
|
|
722
|
+
enforcement,
|
|
723
|
+
enforcementEvidence: schedule?.evidence ?? null,
|
|
724
|
+
lastRun: run?.state ?? 'unknown',
|
|
725
|
+
lastRunEvidence: run?.evidence ?? null,
|
|
726
|
+
lastRunAt: run?.receipt?.finishedAt ?? run?.receipt?.startedAt ?? null,
|
|
727
|
+
agree,
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
646
732
|
function gatherConfig() {
|
|
647
733
|
const cfg = readJSON(CONFIG_PATH) || {};
|
|
648
734
|
const credential = openRouterCredentialStatus({ cwd: process.cwd() });
|
|
649
735
|
const schedule = nightlyStatus();
|
|
650
736
|
const bool = (v) => (v === true ? true : v === false ? false : null);
|
|
737
|
+
const nightly = reconcileNightly({ choice: bool(cfg.nightly), schedule });
|
|
651
738
|
const unavailable = unsupportedConfigControls();
|
|
652
739
|
if (!schedule.artifact.supported) {
|
|
653
740
|
unavailable.push({
|
|
@@ -662,7 +749,7 @@ function gatherConfig() {
|
|
|
662
749
|
values: {
|
|
663
750
|
openrouterKey: credential.configured, // boolean only — never the secret itself
|
|
664
751
|
provider: typeof cfg.provider === 'string' && cfg.provider ? cfg.provider : null,
|
|
665
|
-
nightly:
|
|
752
|
+
nightly: nightly.value,
|
|
666
753
|
routing: cfg.routing === 'off' ? 'off' : cfg.routing === 'auto' ? 'auto' : null,
|
|
667
754
|
qeFleet: bool(cfg.qeFleet),
|
|
668
755
|
},
|
|
@@ -675,7 +762,7 @@ function gatherConfig() {
|
|
|
675
762
|
unavailable,
|
|
676
763
|
runtime: {
|
|
677
764
|
openrouterKey: credential,
|
|
678
|
-
nightly: schedule,
|
|
765
|
+
nightly: { ...schedule, facts: nightly.facts },
|
|
679
766
|
},
|
|
680
767
|
};
|
|
681
768
|
}
|
|
@@ -791,8 +878,21 @@ function gatherBrainProfile() {
|
|
|
791
878
|
: installed.stores.length === 1
|
|
792
879
|
? PROFILE_RUVECTOR
|
|
793
880
|
: PROFILE_COMPLETE;
|
|
881
|
+
// THE "— stores · 0 MB" LIE (console audit 2026-09-11). `choices.complete` was rendered from
|
|
882
|
+
// `source` — the restore BUNDLE at COMPLETE_BRAIN_SOURCE, which on an installed host resolves to
|
|
883
|
+
// <runtime>/dist/ruvnet-brain and never exists after install — while `installed`, measured three
|
|
884
|
+
// lines up (184 stores, 979 MB on the owner's machine), went unused. discoverStoreFamilies()
|
|
885
|
+
// swallows ENOENT into [], so a complete brain rendered as zero and wore an `active` badge over it.
|
|
886
|
+
// Two different questions are now answered separately: what is INSTALLED here, and by which path
|
|
887
|
+
// Apply→complete could RESTORE it here. The path order mirrors saveBrainProfile()'s own branches
|
|
888
|
+
// (local bundle, then the signed forge-update download, then nothing), so the card describes the
|
|
889
|
+
// exact mechanism Apply would take — never a hopeful one, never a number nobody measured.
|
|
794
890
|
const source = measureBrainProfile(COMPLETE_BRAIN_SOURCE);
|
|
891
|
+
const bundlePresent = source.stores.includes(PROFILE_RUVECTOR) && source.storeCount > 1;
|
|
795
892
|
const updaterAvailable = fs.existsSync(path.join(INSTALLED_KB, 'forge-update.mjs'));
|
|
893
|
+
const restoreVia = bundlePresent ? 'local-bundle' : updaterAvailable ? 'signed-download' : null;
|
|
894
|
+
const completeInstalled = actual === PROFILE_COMPLETE;
|
|
895
|
+
const restorePath = COMPLETE_BRAIN_SOURCE.replace(CONSOLE_ROOT, '~');
|
|
796
896
|
return {
|
|
797
897
|
path: INSTALLED_KB.replace(CONSOLE_ROOT, '~'),
|
|
798
898
|
values: { brainProfile: actual },
|
|
@@ -803,10 +903,20 @@ function gatherBrainProfile() {
|
|
|
803
903
|
installed,
|
|
804
904
|
choices: {
|
|
805
905
|
complete: {
|
|
806
|
-
available:
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
906
|
+
available: restoreVia !== null,
|
|
907
|
+
// From the INSTALLED brain when complete is what is installed; from the local bundle when one
|
|
908
|
+
// exists to restore from; otherwise null — never a 0 that nothing measured.
|
|
909
|
+
storeCount: completeInstalled ? installed.storeCount : bundlePresent ? source.storeCount : null,
|
|
910
|
+
bytes: completeInstalled ? installed.bytes : bundlePresent ? source.bytes : null,
|
|
911
|
+
installed: completeInstalled ? { storeCount: installed.storeCount, bytes: installed.bytes } : null,
|
|
912
|
+
restoreBundle: {
|
|
913
|
+
present: bundlePresent,
|
|
914
|
+
path: restorePath,
|
|
915
|
+
storeCount: bundlePresent ? source.storeCount : null,
|
|
916
|
+
bytes: bundlePresent ? source.bytes : null,
|
|
917
|
+
},
|
|
918
|
+
updaterAvailable,
|
|
919
|
+
restoreVia,
|
|
810
920
|
},
|
|
811
921
|
ruvector: {
|
|
812
922
|
available: installed.stores.includes(PROFILE_RUVECTOR)
|
|
@@ -1960,6 +2070,34 @@ function readInstallChannel() {
|
|
|
1960
2070
|
* practice, because the check that did exist only ever triggered a blocking recompute rather than an
|
|
1961
2071
|
* honest stale-serve.
|
|
1962
2072
|
*/
|
|
2073
|
+
/**
|
|
2074
|
+
* THREE VERSIONS ON ONE PAGE (console audit 2026-09-11): the header chip said v4.3.21 (running
|
|
2075
|
+
* brain), the Install-channel row said v4.3.22 (plugin cache), the installed KB's RVF-GENERATIONS.json
|
|
2076
|
+
* said 4.3.10 — and nothing said they differed. Each is read from its own file and LABELED. `agree`
|
|
2077
|
+
* is true only when every measured one is identical, null when fewer than two could be read. A
|
|
2078
|
+
* version that cannot be read is null, never guessed.
|
|
2079
|
+
*/
|
|
2080
|
+
export function versionFacts({ release = null } = {}) {
|
|
2081
|
+
const gen = readJSON(path.join(INSTALLED_KB, 'RVF-GENERATIONS.json'));
|
|
2082
|
+
const kbGeneration = typeof gen?.brainVersion === 'string' && gen.brainVersion ? gen.brainVersion.replace(/^v/, '') : null;
|
|
2083
|
+
const runningBrain = brainVersionOnDisk();
|
|
2084
|
+
let installedPlugin = null;
|
|
2085
|
+
try { installedPlugin = readInstallChannel().version || null; } catch { installedPlugin = null; }
|
|
2086
|
+
// A CORPUS RELEASE IS NOT A VERSION (ADR-086 step 16). `releases/latest` can now carry a
|
|
2087
|
+
// `corpus-sha256-<64 hex>` content address, and stripping a leading "v" off one yields a 78-char
|
|
2088
|
+
// string that this panel would have printed in the "latest release" slot beside `4.3.22`-shaped
|
|
2089
|
+
// numbers — in the one panel whose whole job is to make version disagreement visible. That is not
|
|
2090
|
+
// a version disagreement; it is a category error, and showing it as one would be the panel lying.
|
|
2091
|
+
// So the two identities are reported in two fields, each labeled for what it actually is.
|
|
2092
|
+
const rawTag = typeof release?.tag === 'string' && release.tag ? release.tag : null;
|
|
2093
|
+
const corpusTag = isCorpusReleaseTag(rawTag) ? rawTag : null;
|
|
2094
|
+
const latestRelease = rawTag && !corpusTag ? rawTag.replace(/^v/, '') : null;
|
|
2095
|
+
const known = [kbGeneration, runningBrain, installedPlugin].filter(Boolean);
|
|
2096
|
+
const agree = known.length < 2 ? null : known.every((x) => x === known[0]);
|
|
2097
|
+
return { kbGeneration, runningBrain, installedPlugin, latestRelease, latestCorpusRelease: corpusTag,
|
|
2098
|
+
agree, known: known.length };
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1963
2101
|
async function gatherTrust() {
|
|
1964
2102
|
// COLD ONLY: no successful release read has ever landed, so there is nothing to withhold or serve
|
|
1965
2103
|
// stale — the one exception serveCached() itself carves out for its own caches.
|
|
@@ -1971,7 +2109,7 @@ async function gatherTrust() {
|
|
|
1971
2109
|
const generatedAt = new Date().toISOString();
|
|
1972
2110
|
const data = { generatedAt, release };
|
|
1973
2111
|
if (release.ok) { TRUST_CACHE = { at: Date.parse(generatedAt), data }; saveConsoleCache(); }
|
|
1974
|
-
return { ...data, channel: readInstallChannel(), sbom: readSbom(), ...freshnessOf(generatedAt) };
|
|
2112
|
+
return { ...data, channel: readInstallChannel(), sbom: readSbom(), versions: versionFacts({ release }), ...freshnessOf(generatedAt) };
|
|
1975
2113
|
}
|
|
1976
2114
|
|
|
1977
2115
|
// WARM — including over-ceiling. Never await the network here; hand back what we measured, say
|
|
@@ -1980,7 +2118,7 @@ async function gatherTrust() {
|
|
|
1980
2118
|
if (fresh.stale) kickTrustRefresh();
|
|
1981
2119
|
// Disk facts stay live even when the release read is served from cache — the SBOM file and install
|
|
1982
2120
|
// channel can change (a fresh `npm run sbom`, a plugin update) between two calls inside the ceiling.
|
|
1983
|
-
return { ...TRUST_CACHE.data, channel: readInstallChannel(), sbom: readSbom(), ...fresh };
|
|
2121
|
+
return { ...TRUST_CACHE.data, channel: readInstallChannel(), sbom: readSbom(), versions: versionFacts({ release: TRUST_CACHE.data.release }), ...fresh };
|
|
1984
2122
|
}
|
|
1985
2123
|
|
|
1986
2124
|
// ── Assemble the read-models ─────────────────────────────────────────────────────────────────────
|
|
@@ -2864,6 +3002,10 @@ function startServer({ port = Number(process.env.CONSOLE_PORT) || 7411, open = f
|
|
|
2864
3002
|
if (req.method === 'GET' && url === '/api/lessons') return sendJSON(res, 200, gatherLessons());
|
|
2865
3003
|
if (req.method === 'GET' && url === '/api/trust') return sendJSON(res, 200, await gatherTrust());
|
|
2866
3004
|
if (req.method === 'GET' && url === '/tips') { req.url = '/tips.html'; return serveStatic(req, res); }
|
|
3005
|
+
// What's in the brain: derived from the installed root's own receipts on every request (read-only,
|
|
3006
|
+
// no network, ~ms) — a coverage listing served from a stale cache would defeat its one purpose.
|
|
3007
|
+
if (req.method === 'GET' && url === '/api/scope') return sendJSON(res, 200, gatherScope());
|
|
3008
|
+
if (req.method === 'GET' && url === '/scope') { req.url = '/scope.html'; return serveStatic(req, res); }
|
|
2867
3009
|
if (req.method === 'POST') {
|
|
2868
3010
|
const body = await readBody(req);
|
|
2869
3011
|
if (body.token !== TOKEN) return sendJSON(res, 403, { error: 'bad or missing token' });
|
|
@@ -3046,7 +3188,155 @@ if (process.argv[1] && path.resolve(process.argv[1]).endsWith('onboarding-consol
|
|
|
3046
3188
|
else { console.log(`\n onboarding-console — the RuvNet Brain configure page\n\n --serve [--open] start or safely replace the scoped local server\n --runtime-status print candidate, receipt, and live runtime status\n --print-state print the read-only state JSON and exit (for tests)\n --print-stack print the stack audit JSON and exit\n`); }
|
|
3047
3189
|
}
|
|
3048
3190
|
|
|
3191
|
+
// ── "What's in the brain" (the scope page) ─────────────────────────────────────────────────────────
|
|
3192
|
+
// Every verdict comes from the installed brain's OWN receipts, never from the shipped `status` word.
|
|
3193
|
+
// Measured 2026-09-11 (KB audit): release-projection.mjs:80 stamps every seeded row CURRENT — 63 real
|
|
3194
|
+
// rows had sourceCommit ≠ upstream.sha. GitHub `updatedAt` is metadata (222/227 repos moved on it in
|
|
3195
|
+
// one week) and `pushedAt` leads the default-branch HEAD on 39/227; only `committedAt` dates the commit
|
|
3196
|
+
// the brain ingests. Gists have no branches, so their `updatedAt` IS the content date — and the shipped
|
|
3197
|
+
// COVERAGE row carries no sourceCommit for them (479/479 null); the gist receipt set
|
|
3198
|
+
// (ruv-gists.sources.json → versionSha) is the brain's record of which revision it holds.
|
|
3199
|
+
//
|
|
3200
|
+
// The truth per row is `brainSha === upstreamSha`. A store with no recorded source commit is
|
|
3201
|
+
// "unverified" — never promoted to current on the strength of a date or a label. A store whose
|
|
3202
|
+
// .big.rvf is not at the installed root is "not in the brain", whatever the record says.
|
|
3203
|
+
const SCOPE_SENTENCE = "If rUv's last change to a repo is on or before the date the brain read it, everything in that repo is in the brain. If it's after, the brain is behind on that repo by the difference.";
|
|
3204
|
+
const SCOPE_DAY_MS = 86_400_000;
|
|
3205
|
+
const SCOPE_STALE_DAYS = 7;
|
|
3206
|
+
// A row's `desc` is what the thing DOES, so the page's search answers "is the stuff on Federation in
|
|
3207
|
+
// Ruflo?" — a description question — not only name lookups. Bounded so a README pasted into a card
|
|
3208
|
+
// cannot bloat 700 rows.
|
|
3209
|
+
const SCOPE_DESC_MAX = 200;
|
|
3210
|
+
const scopeBounded = (s) => (s.length > SCOPE_DESC_MAX ? `${s.slice(0, SCOPE_DESC_MAX - 1)}…` : s);
|
|
3211
|
+
|
|
3212
|
+
/**
|
|
3213
|
+
* Pure: the installed brain's capability-cards.md → Map(lowercased repo/store name → first line of
|
|
3214
|
+
* its card, bounded). The card file is the ONLY per-repo description that ships to a customer
|
|
3215
|
+
* machine (data/ruvnet-registry.json is repo-side and never installs — measured 2026-09-12). Format,
|
|
3216
|
+
* measured on 182 sections: `## <name>`, one line saying what it does, one auto-derivation note.
|
|
3217
|
+
*/
|
|
3218
|
+
function parseCapabilityCards(text) {
|
|
3219
|
+
const cards = new Map();
|
|
3220
|
+
if (typeof text !== 'string') return cards;
|
|
3221
|
+
const lines = text.split('\n');
|
|
3222
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3223
|
+
const h = /^##\s+(\S.*?)\s*$/.exec(lines[i]);
|
|
3224
|
+
if (!h) continue;
|
|
3225
|
+
let j = i + 1;
|
|
3226
|
+
while (j < lines.length && !lines[j].trim()) j += 1;
|
|
3227
|
+
const first = j < lines.length && !lines[j].startsWith('#') ? lines[j].trim() : '';
|
|
3228
|
+
if (first) cards.set(h[1].toLowerCase(), scopeBounded(first));
|
|
3229
|
+
}
|
|
3230
|
+
return cards;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
function scopeDesc(row, { isGist, store, gistId, gistSources, cards }) {
|
|
3234
|
+
if (isGist) {
|
|
3235
|
+
// A gist's only words are its file names (COVERAGE upstream.files; the receipt's files as fallback).
|
|
3236
|
+
const files = row.upstream?.files ?? gistSources?.gists?.[gistId]?.files ?? null;
|
|
3237
|
+
const names = Array.isArray(files) ? files.map((f) => (typeof f === 'string' ? f : f?.filename)).filter(Boolean) : [];
|
|
3238
|
+
return names.length ? scopeBounded(names.join(' · ')) : null;
|
|
3239
|
+
}
|
|
3240
|
+
const byName = cards?.get(String(row.name ?? '').toLowerCase());
|
|
3241
|
+
if (byName) return byName;
|
|
3242
|
+
return (store && cards?.get(String(store).toLowerCase())) || null;
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
function scopeRow(row, { generations, gistSources, installedStores, cards }) {
|
|
3246
|
+
const isGist = row.kind === 'gist';
|
|
3247
|
+
const store = row.artifact?.store ?? null;
|
|
3248
|
+
const ledger = store ? (generations?.stores?.[store] ?? null) : null;
|
|
3249
|
+
const gistId = isGist ? String(row.key ?? '').replace(/^gist:/, '') : null;
|
|
3250
|
+
const upstreamSha = row.upstream?.sha ?? null;
|
|
3251
|
+
const desc = scopeDesc(row, { isGist, store, gistId, gistSources, cards });
|
|
3252
|
+
|
|
3253
|
+
let brainSha = row.artifact?.sourceCommit ?? null;
|
|
3254
|
+
if (brainSha == null) brainSha = isGist ? (gistSources?.gists?.[gistId]?.versionSha ?? null) : (ledger?.sourceCommit ?? null);
|
|
3255
|
+
|
|
3256
|
+
const ruvChangedAt = (isGist ? row.upstream?.updatedAt : row.upstream?.committedAt) ?? null;
|
|
3257
|
+
const brainReadAt = row.artifact?.ingestedAt ?? ledger?.builtUtc ?? null;
|
|
3258
|
+
|
|
3259
|
+
let installed = Boolean(store) && installedStores.has(store);
|
|
3260
|
+
// With a gist receipt set present, a gist the set does not enumerate is not in the store.
|
|
3261
|
+
if (installed && isGist && gistSources?.gists && !(gistId in gistSources.gists)) installed = false;
|
|
3262
|
+
|
|
3263
|
+
let bucket;
|
|
3264
|
+
if (!installed) bucket = 'not-in-brain';
|
|
3265
|
+
else if (brainSha && upstreamSha) bucket = brainSha === upstreamSha ? 'current' : 'behind';
|
|
3266
|
+
else bucket = 'unverified';
|
|
3267
|
+
|
|
3268
|
+
let behindDays = null;
|
|
3269
|
+
if (bucket === 'behind' && ruvChangedAt && brainReadAt) {
|
|
3270
|
+
const d = (Date.parse(ruvChangedAt) - Date.parse(brainReadAt)) / SCOPE_DAY_MS;
|
|
3271
|
+
behindDays = Number.isFinite(d) ? Math.max(0, Math.round(d)) : null;
|
|
3272
|
+
}
|
|
3273
|
+
return { kind: row.kind, name: row.name, url: row.url ?? null, store, desc, bucket, ruvChangedAt, brainReadAt, behindDays, brainSha, upstreamSha };
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
const scopeNewestFirst = (a, b) => {
|
|
3277
|
+
if (a.ruvChangedAt == null && b.ruvChangedAt == null) return String(a.name).localeCompare(String(b.name));
|
|
3278
|
+
if (a.ruvChangedAt == null) return 1;
|
|
3279
|
+
if (b.ruvChangedAt == null) return -1;
|
|
3280
|
+
return a.ruvChangedAt < b.ruvChangedAt ? 1 : a.ruvChangedAt > b.ruvChangedAt ? -1 : String(a.name).localeCompare(String(b.name));
|
|
3281
|
+
};
|
|
3282
|
+
|
|
3283
|
+
/** Pure: coverage + ledger + gist receipts + the set of installed store names → the page's payload. */
|
|
3284
|
+
function computeScope({ coverage, generations, gistSources, installedStores, root, cards = new Map(), now = Date.now() }) {
|
|
3285
|
+
const empty = () => ({ total: 0, current: 0, behind: 0, unverified: 0, notInBrain: 0, ineligible: 0 });
|
|
3286
|
+
const counts = { repos: empty(), gists: empty() };
|
|
3287
|
+
const out = { repos: [], gists: [] };
|
|
3288
|
+
const covered = new Set();
|
|
3289
|
+
for (const row of coverage.rows ?? []) {
|
|
3290
|
+
const kind = row.kind === 'gist' ? 'gists' : 'repos';
|
|
3291
|
+
if (row.artifact?.store) covered.add(row.artifact.store);
|
|
3292
|
+
counts[kind].total += 1;
|
|
3293
|
+
if (row.disposition !== 'eligible') { counts[kind].ineligible += 1; continue; }
|
|
3294
|
+
const r = scopeRow(row, { generations, gistSources, installedStores, cards });
|
|
3295
|
+
counts[kind][r.bucket === 'not-in-brain' ? 'notInBrain' : r.bucket] += 1;
|
|
3296
|
+
out[kind].push(r);
|
|
3297
|
+
}
|
|
3298
|
+
out.repos.sort(scopeNewestFirst);
|
|
3299
|
+
out.gists.sort(scopeNewestFirst);
|
|
3300
|
+
const observedAt = typeof coverage.observedAt === 'string' ? coverage.observedAt : null;
|
|
3301
|
+
const ageMs = observedAt ? now - Date.parse(observedAt) : NaN;
|
|
3302
|
+
const ageDays = Number.isFinite(ageMs) ? Math.round((ageMs / SCOPE_DAY_MS) * 10) / 10 : null;
|
|
3303
|
+
return {
|
|
3304
|
+
available: true,
|
|
3305
|
+
root,
|
|
3306
|
+
observedAt,
|
|
3307
|
+
ageDays,
|
|
3308
|
+
stale: ageDays == null || ageDays > SCOPE_STALE_DAYS,
|
|
3309
|
+
brainVersion: generations?.brainVersion ?? null,
|
|
3310
|
+
releaseTag: generations?.releaseTag ?? null,
|
|
3311
|
+
sentence: SCOPE_SENTENCE,
|
|
3312
|
+
counts,
|
|
3313
|
+
repos: out.repos,
|
|
3314
|
+
gists: out.gists,
|
|
3315
|
+
installedStoreCount: installedStores.size,
|
|
3316
|
+
installedOutsideCoverage: [...installedStores].filter((s) => !covered.has(s)).sort(),
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
/** Read the installed root and compute. Read-only; no network; returns `available: false` rather than a guess. */
|
|
3321
|
+
function gatherScope() {
|
|
3322
|
+
const readJson = (rel) => { try { return JSON.parse(fs.readFileSync(path.join(INSTALLED_KB, rel), 'utf8')); } catch { return null; } };
|
|
3323
|
+
const coverage = readJson('COVERAGE.json');
|
|
3324
|
+
if (!coverage || !Array.isArray(coverage.rows)) {
|
|
3325
|
+
return { available: false, reason: `no readable COVERAGE.json at ${INSTALLED_KB}`, root: INSTALLED_KB, sentence: SCOPE_SENTENCE, repos: [], gists: [], counts: {}, installedOutsideCoverage: [], installedStoreCount: 0 };
|
|
3326
|
+
}
|
|
3327
|
+
let installedStores = new Set();
|
|
3328
|
+
try { installedStores = new Set(fs.readdirSync(INSTALLED_KB).filter((f) => f.endsWith('.big.rvf')).map((f) => f.slice(0, -'.big.rvf'.length))); }
|
|
3329
|
+
catch { /* unreadable root reads as nothing installed — every row then says "not in the brain" */ }
|
|
3330
|
+
let cardsText = null;
|
|
3331
|
+
try { cardsText = fs.readFileSync(path.join(INSTALLED_KB, 'capability-cards.md'), 'utf8'); } catch { /* no cards → every desc is null, never invented */ }
|
|
3332
|
+
return computeScope({ coverage, generations: readJson('RVF-GENERATIONS.json'), gistSources: readJson('ruv-gists.sources.json'),
|
|
3333
|
+
installedStores, root: INSTALLED_KB, cards: parseCapabilityCards(cardsText) });
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3049
3336
|
export {
|
|
3337
|
+
gatherScope,
|
|
3338
|
+
computeScope,
|
|
3339
|
+
parseCapabilityCards,
|
|
3050
3340
|
gatherState,
|
|
3051
3341
|
gatherStack,
|
|
3052
3342
|
gatherTrust,
|
|
@@ -3063,6 +3353,8 @@ export {
|
|
|
3063
3353
|
saveBrainProfile,
|
|
3064
3354
|
gatherRouterEngine,
|
|
3065
3355
|
autoEligibleIds,
|
|
3356
|
+
gatherConfig,
|
|
3357
|
+
gatherSavings,
|
|
3066
3358
|
};
|
|
3067
3359
|
// Exported for the cross-project cache-isolation test (console-cache-scope.test.mjs). serveCached's
|
|
3068
3360
|
// scopeKey is the guard that stops one project's cached state being served for another.
|