ruvnet-brain 4.0.36 → 4.2.2-dev
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 +28 -5
- package/bin/install.mjs +405 -23
- package/data/model-catalog.json +104 -15
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +72 -9
- package/plugin/commands/configure.md +67 -21
- package/plugin/commands/rvcb.md +72 -9
- package/plugin/hooks/codex-hooks.json +40 -33
- package/plugin/hooks/hook-contracts.json +14 -24
- package/plugin/hooks/hooks.json +7 -42
- package/plugin/mcp/server.mjs +23 -6
- package/plugin/scripts/adr-currency-gate.mjs +150 -0
- package/plugin/scripts/capability-registry.mjs +10 -1
- package/plugin/scripts/codex-hook-adapter.mjs +121 -19
- package/plugin/scripts/codex-hook-wrapper.mjs +61 -4
- package/plugin/scripts/continuation-gate.mjs +148 -8
- package/plugin/scripts/decision-gate.mjs +428 -0
- package/plugin/scripts/decision-outcomes.mjs +231 -0
- package/plugin/scripts/degradation-watch.mjs +271 -0
- package/plugin/scripts/ground-ruvnet.sh +51 -11
- package/plugin/scripts/hijack-ruvnet.sh +11 -3
- package/plugin/scripts/hook-registry.mjs +48 -3
- package/plugin/scripts/hook-shim.mjs +58 -3
- package/plugin/scripts/identifier-preflight.mjs +134 -0
- package/plugin/scripts/learn-capture.sh +50 -1
- package/plugin/scripts/learn-flush.mjs +5 -5
- package/plugin/scripts/lesson-bridge.mjs +343 -0
- package/plugin/scripts/lesson-hooks.sh +26 -0
- package/plugin/scripts/lesson-promote.mjs +50 -0
- package/plugin/scripts/lesson-store.mjs +6 -1
- package/plugin/scripts/mcp-readiness.mjs +107 -0
- package/plugin/scripts/protect-brain-state.sh +9 -0
- package/plugin/scripts/runtime-preferences.mjs +40 -0
- package/plugin/scripts/session-snapshot-hook.mjs +15 -6
- package/plugin/scripts/spend-guard.mjs +125 -0
- package/plugin/scripts/unprompted-runtime.mjs +12 -2
- package/plugin/scripts/update-apply.mjs +7 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +20 -5
- package/plugin/skills/ruvnet-brain/SKILL.md +3 -3
- package/scripts/brain-score.mjs +261 -0
- package/scripts/brain-stamp.mjs +5 -1
- package/scripts/build-bundle.mjs +38 -4
- package/scripts/card-from-source.mjs +114 -0
- package/scripts/console-engine.mjs +1 -1
- package/scripts/corpus-candidate.mjs +294 -0
- package/scripts/corpus-reconcile.mjs +273 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/health-repair.mjs +11 -2
- package/scripts/ingest-new-repos.mjs +122 -0
- package/scripts/ingest-repo.mjs +66 -6
- package/scripts/learning-replay-cli.mjs +8 -3
- package/scripts/learning-replay-fixture.mjs +25 -6
- package/scripts/learning-replay-proof.mjs +38 -0
- package/scripts/nightly-wrapper.sh +55 -7
- package/scripts/onboarding-console.mjs +21 -1
- package/scripts/org-repo-count.mjs +119 -0
- package/scripts/rebuild-gists-from-receipts.mjs +246 -0
- package/scripts/release-transaction.mjs +30 -2
- package/scripts/release.mjs +147 -0
- package/scripts/repo-count-detector.mjs +62 -0
- package/scripts/restore-local-ingests.mjs +116 -0
- package/scripts/rvf-generation.mjs +44 -0
- package/scripts/selfcheck.mjs +9 -1
- package/scripts/stabilization-receipt.mjs +11 -1
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-commands.mjs +117 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brain-score.mjs — the ONLY sanctioned source of a score about this product.
|
|
3
|
+
*
|
|
4
|
+
* THE DEFECT THIS REPLACES, measured 2026-08-13. Asked to grade the architecture, I counted files
|
|
5
|
+
* — stores/repos, cards/stores — called the result 52/100, and reported it as an architecture
|
|
6
|
+
* grade. Six graders sat unused in this same repo. The real numbers were nothing like 52, and they
|
|
7
|
+
* disagreed with each other in ways that mattered:
|
|
8
|
+
*
|
|
9
|
+
* grounded 100/100 does it fabricate? (evals/baseline.json)
|
|
10
|
+
* routed 63/80 does it reach the right store? same file, same run
|
|
11
|
+
* panel ~56 independent multi-vendor grade (data/grade-*.json)
|
|
12
|
+
*
|
|
13
|
+
* A single number could not have carried that. My 52 did not measure quality at all; it measured
|
|
14
|
+
* CATALOGUE COVERAGE and wore a quality label. That is the specific error this file makes
|
|
15
|
+
* structurally impossible: dimensions carry a `kind`, and `composite()` REFUSES to average across
|
|
16
|
+
* kinds. A coverage percentage and a quality percentage are answers to different questions, and
|
|
17
|
+
* collapsing them tells the owner his working product is failing while his users report it works.
|
|
18
|
+
*
|
|
19
|
+
* THE SECOND DEFECT, committed while correcting the first. Having found `grounded 100/100`, I
|
|
20
|
+
* quoted it as current. It was recorded 2026-07-10 — 34 days and ~40 commits earlier. The existing
|
|
21
|
+
* claims gate passed it because it checks INTERNAL consistency (k<=n, lo<=p<=hi) and never asks
|
|
22
|
+
* WHEN. So every dimension here declares `maxAgeDays`, and a reading past it is `stale` — reported,
|
|
23
|
+
* never silently used, and never the basis of a claim.
|
|
24
|
+
*
|
|
25
|
+
* This is not new machinery. It is ADR-065's own rule — facts are GENERATED, behaviours are TESTED
|
|
26
|
+
* — applied to the one class of fact that was still being typed by hand. `orgTotalApprox: 248` was
|
|
27
|
+
* this exact bug about a repo count; it was wrong by 48 in two files at once. `sync-version.mjs`
|
|
28
|
+
* and `sync-census.mjs` fixed it for versions and counts. Scores were never brought along.
|
|
29
|
+
*/
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
35
|
+
const DAY = 86_400_000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Every dimension names the QUESTION it answers, because a number whose question is unstated is
|
|
39
|
+
* the thing that produced the 52. `kind` is load-bearing: see `composite()`.
|
|
40
|
+
*/
|
|
41
|
+
export const DIMENSIONS = [
|
|
42
|
+
{
|
|
43
|
+
key: 'grounded',
|
|
44
|
+
kind: 'quality',
|
|
45
|
+
question: 'When it answers, is the answer bound to real retrieved source rather than invented?',
|
|
46
|
+
artifact: 'evals/baseline.json',
|
|
47
|
+
maxAgeDays: 14,
|
|
48
|
+
read: (j) => ({ value: pct(j?.score?.grounded), detail: frac(j?.score?.grounded), at: j?.recorded }),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'routed',
|
|
52
|
+
kind: 'quality',
|
|
53
|
+
question: 'Does a by-description query reach the store that actually holds the answer?',
|
|
54
|
+
artifact: 'evals/baseline.json',
|
|
55
|
+
maxAgeDays: 14,
|
|
56
|
+
read: (j) => ({ value: pct(j?.score?.routed), detail: frac(j?.score?.routed), at: j?.recorded }),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'abstained',
|
|
60
|
+
kind: 'quality',
|
|
61
|
+
question: 'When it does NOT know, does it decline instead of producing something?',
|
|
62
|
+
artifact: 'evals/baseline.json',
|
|
63
|
+
maxAgeDays: 14,
|
|
64
|
+
read: (j) => ({ value: pct(j?.score?.abstain), detail: frac(j?.score?.abstain), at: j?.recorded }),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: 'panelStrict',
|
|
68
|
+
kind: 'quality',
|
|
69
|
+
question: 'How do INDEPENDENT-vendor graders score the answers, strictly? (ADR-0002 gate)',
|
|
70
|
+
artifact: 'data/grade-*.json',
|
|
71
|
+
maxAgeDays: 30,
|
|
72
|
+
read: null, // aggregated across files — see readPanel()
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'catalogue',
|
|
76
|
+
kind: 'coverage',
|
|
77
|
+
question: 'What FRACTION of rUv\'s live repos has a store at all? (NOT a quality measure)',
|
|
78
|
+
artifact: 'data/org-repo-count.json + the live store root',
|
|
79
|
+
maxAgeDays: 7,
|
|
80
|
+
read: null, // derived — see readCatalogue()
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: 'routable',
|
|
84
|
+
kind: 'coverage',
|
|
85
|
+
question: 'What fraction of BUILT stores can a by-description query actually reach?',
|
|
86
|
+
artifact: 'capability-cards.md at the store root',
|
|
87
|
+
maxAgeDays: 7,
|
|
88
|
+
read: null,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Is this store reachable by a by-description query? ALIAS-AWARE, because the router is.
|
|
94
|
+
*
|
|
95
|
+
* Pure and exported so the rule can be tested without a live store root — the previous version was
|
|
96
|
+
* an inline `cards.has(s)` buried in an async reader, which is why nobody noticed it disagreed with
|
|
97
|
+
* the router. `resolve` is injected rather than imported here so a fixture can supply its own
|
|
98
|
+
* registry; production passes `repositoryNames` from kb/card-lane.mjs, the router's own resolver.
|
|
99
|
+
*/
|
|
100
|
+
export function isRoutable(store, cards, root, resolve) {
|
|
101
|
+
return resolve(store, root).some((name) => cards.has(name));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const pct = (s) => (s && Number.isFinite(s.p) ? Math.round(s.p * 1000) / 10 : null);
|
|
105
|
+
const frac = (s) => (s && Number.isFinite(s.k) ? `${s.k}/${s.n}` : null);
|
|
106
|
+
|
|
107
|
+
function readJson(rel) {
|
|
108
|
+
try { return JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8')); } catch { return null; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Aggregate the multi-vendor panel across whatever stores have actually been graded. */
|
|
112
|
+
function readPanel() {
|
|
113
|
+
const dir = path.join(ROOT, 'data');
|
|
114
|
+
let files = [];
|
|
115
|
+
try { files = fs.readdirSync(dir).filter((f) => /^grade-.*\.json$/.test(f)); } catch { /* none */ }
|
|
116
|
+
const rows = files.map((f) => ({ f, j: readJson(path.join('data', f)) })).filter((r) => r.j?.summary);
|
|
117
|
+
if (!rows.length) return { value: null, detail: null, at: null };
|
|
118
|
+
const strict = rows.map((r) => r.j.summary.avgStrict).filter(Number.isFinite);
|
|
119
|
+
if (!strict.length) return { value: null, detail: null, at: null };
|
|
120
|
+
const at = rows
|
|
121
|
+
.map((r) => { try { return fs.statSync(path.join(dir, r.f)).mtime.toISOString(); } catch { return null; } })
|
|
122
|
+
.filter(Boolean).sort().pop();
|
|
123
|
+
return {
|
|
124
|
+
value: Math.round((strict.reduce((a, b) => a + b, 0) / strict.length) * 10) / 10,
|
|
125
|
+
detail: `${strict.length} store(s) graded`,
|
|
126
|
+
at,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Coverage is DERIVED from the live store root, never from a number in a doc. Returns null rather
|
|
132
|
+
* than a guess when the denominator is unknown — a wrong denominator still renders, which is worse
|
|
133
|
+
* than an absent one.
|
|
134
|
+
*/
|
|
135
|
+
export async function readCoverage(rootOverride) {
|
|
136
|
+
const { storeRoot, storesAt, cardsAt } = await import('../kb/store-root.mjs');
|
|
137
|
+
// ROUTABILITY IS ALIAS-AWARE, BECAUSE THE ROUTER IS.
|
|
138
|
+
//
|
|
139
|
+
// `carded` was `stores.filter((s) => cards.has(s))` — a direct name match against card HEADINGS,
|
|
140
|
+
// while the real router resolves a store through `repositoryNames()` first. So a store reachable
|
|
141
|
+
// ONLY under an alias counted as unroutable, and this metric under-reported the brain's own
|
|
142
|
+
// reachability: measured 2026-08-19, 30 stores called unroutable when 26 actually are —
|
|
143
|
+
// `metaharness` among the four, which is reachable through the `agent-harness-generator` card.
|
|
144
|
+
//
|
|
145
|
+
// That is not a rounding error, it is the same alias-blindness ADR-058 records as open in
|
|
146
|
+
// `darkStores()`, and it already cost something real: a false "dark store" reading led to a
|
|
147
|
+
// DUPLICATE `## metaharness` card being added, which collided with `agent-harness-generator` and
|
|
148
|
+
// broke alias resolution outright. A metric that cannot see aliases manufactures work that
|
|
149
|
+
// damages the thing it measures.
|
|
150
|
+
const { repositoryNames } = await import('../kb/card-lane.mjs');
|
|
151
|
+
const root = rootOverride ?? storeRoot();
|
|
152
|
+
// NEVER-MATERIALIZED, THE SAME AMBIGUITY `restore-local-ingests.mjs`'s `classify()` WAS FIXED
|
|
153
|
+
// FOR (PR #143, Night 1) — but never carried here. `storesAt()`/`cardsAt()` silently return
|
|
154
|
+
// `[]` on ENOENT, so a host whose store root was never restored (a fresh checkout, this very
|
|
155
|
+
// nightly agent's own ephemeral container) reads IDENTICALLY to a materialized root that
|
|
156
|
+
// genuinely has zero coverage. Checked once, here, before either derived number is computed —
|
|
157
|
+
// a wrong `0` still renders, which is worse than an honestly unmeasured one.
|
|
158
|
+
const neverMaterialized = !fs.existsSync(root);
|
|
159
|
+
const stores = storesAt(root);
|
|
160
|
+
const cards = new Set(cardsAt(root));
|
|
161
|
+
const org = readJson('data/org-repo-count.json');
|
|
162
|
+
const total = Number.isFinite(org?.count) && org.count > 0 ? org.count : null;
|
|
163
|
+
const carded = stores.filter((s) => isRoutable(s, cards, root, repositoryNames)).length;
|
|
164
|
+
const absentDetail = 'store root does not exist on this host (never materialized) — not evidence of live coverage';
|
|
165
|
+
return {
|
|
166
|
+
catalogue: {
|
|
167
|
+
value: neverMaterialized ? null : (total ? Math.round((stores.length / total) * 1000) / 10 : null),
|
|
168
|
+
detail: neverMaterialized ? absentDetail
|
|
169
|
+
: (total ? `${stores.length}/${total} live repos` : `${stores.length} stores, org total UNKNOWN`),
|
|
170
|
+
at: neverMaterialized ? null : (org?.at ?? null),
|
|
171
|
+
},
|
|
172
|
+
routable: {
|
|
173
|
+
value: neverMaterialized || !stores.length ? null : Math.round((carded / stores.length) * 1000) / 10,
|
|
174
|
+
detail: neverMaterialized ? absentDetail : `${carded}/${stores.length} built stores have a card`,
|
|
175
|
+
// Derived from the live store root at this instant, so it is current BY CONSTRUCTION. A
|
|
176
|
+
// read-now value reported as stale would train the reader to ignore the stale flag, which
|
|
177
|
+
// costs more than the flag is worth.
|
|
178
|
+
at: neverMaterialized ? null : new Date().toISOString(),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A reading past its dimension's `maxAgeDays`, or with no timestamp at all, is not current. */
|
|
184
|
+
export function staleness(at, maxAgeDays, now = Date.now()) {
|
|
185
|
+
if (!at) return { stale: true, ageDays: null, why: 'no timestamp recorded' };
|
|
186
|
+
const ms = now - Date.parse(at);
|
|
187
|
+
if (!Number.isFinite(ms)) return { stale: true, ageDays: null, why: 'unparseable timestamp' };
|
|
188
|
+
const ageDays = Math.round((ms / DAY) * 10) / 10;
|
|
189
|
+
return { stale: ageDays > maxAgeDays, ageDays, why: ageDays > maxAgeDays ? `${ageDays}d > ${maxAgeDays}d budget` : null };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function brainScore({ now = Date.now() } = {}) {
|
|
193
|
+
const cov = await readCoverage();
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const d of DIMENSIONS) {
|
|
196
|
+
let r;
|
|
197
|
+
if (d.key === 'panelStrict') r = readPanel();
|
|
198
|
+
else if (cov[d.key]) r = cov[d.key];
|
|
199
|
+
else r = d.read ? d.read(readJson(d.artifact)) : { value: null, detail: null, at: null };
|
|
200
|
+
out.push({
|
|
201
|
+
...d,
|
|
202
|
+
...r,
|
|
203
|
+
...staleness(r.at, d.maxAgeDays, now),
|
|
204
|
+
status: r.value === null ? 'unmeasured' : (staleness(r.at, d.maxAgeDays, now).stale ? 'stale' : 'current'),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* THE STRUCTURAL GUARD. Averaging a coverage percentage with a quality percentage is precisely the
|
|
212
|
+
* 52. It is refused here rather than discouraged in prose, because prose did not stop me.
|
|
213
|
+
*/
|
|
214
|
+
export function composite(dims, kind) {
|
|
215
|
+
if (!kind) return { value: null, refused: 'a score must name WHICH question it answers — pass a kind' };
|
|
216
|
+
const mine = dims.filter((d) => d.kind === kind);
|
|
217
|
+
if (!mine.length) return { value: null, refused: `no dimensions of kind "${kind}"` };
|
|
218
|
+
const usable = mine.filter((d) => d.status === 'current' && d.value !== null);
|
|
219
|
+
if (usable.length < mine.length) {
|
|
220
|
+
return {
|
|
221
|
+
value: null,
|
|
222
|
+
refused: `${mine.length - usable.length} of ${mine.length} ${kind} dimension(s) are stale or unmeasured`,
|
|
223
|
+
missing: mine.filter((d) => !usable.includes(d)).map((d) => `${d.key} (${d.status})`),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { value: Math.round((usable.reduce((a, d) => a + d.value, 0) / usable.length) * 10) / 10, of: usable.length };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const isMain = (() => {
|
|
230
|
+
try { return process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); }
|
|
231
|
+
catch { return false; }
|
|
232
|
+
})();
|
|
233
|
+
|
|
234
|
+
if (isMain) {
|
|
235
|
+
const dims = await brainScore();
|
|
236
|
+
const asJson = process.argv.includes('--json');
|
|
237
|
+
if (asJson) {
|
|
238
|
+
console.log(JSON.stringify({ recorded: new Date().toISOString(), dimensions: dims }, null, 2));
|
|
239
|
+
} else {
|
|
240
|
+
console.log('\nRUVNET-BRAIN — MEASURED, never typed. Each row names the question it answers.\n');
|
|
241
|
+
for (const kind of ['quality', 'coverage']) {
|
|
242
|
+
console.log(` ${kind.toUpperCase()}`);
|
|
243
|
+
for (const d of dims.filter((x) => x.kind === kind)) {
|
|
244
|
+
const v = d.value === null ? 'UNMEASURED' : `${d.value}`.padStart(5);
|
|
245
|
+
const age = d.ageDays === null ? '' : ` ${d.ageDays}d old`;
|
|
246
|
+
const flag = d.status === 'current' ? '' : ` <-- ${d.status.toUpperCase()}${d.why ? ` (${d.why})` : ''}`;
|
|
247
|
+
console.log(` ${d.key.padEnd(13)} ${v} ${(d.detail ?? '').padEnd(26)}${age}${flag}`);
|
|
248
|
+
console.log(` ${d.question}`);
|
|
249
|
+
}
|
|
250
|
+
const c = composite(dims, kind);
|
|
251
|
+
console.log(c.value === null ? ` => NO ${kind} COMPOSITE: ${c.refused}` : ` => ${kind} composite ${c.value} (mean of ${c.of})`);
|
|
252
|
+
console.log('');
|
|
253
|
+
}
|
|
254
|
+
console.log(' A quality number and a coverage number are NEVER averaged. That was the 52.\n');
|
|
255
|
+
}
|
|
256
|
+
const anyStale = dims.some((d) => d.status !== 'current');
|
|
257
|
+
if (process.argv.includes('--require-current') && anyStale) {
|
|
258
|
+
console.error('brain-score: refusing — at least one dimension is stale or unmeasured. Re-run the graders.');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
}
|
package/scripts/brain-stamp.mjs
CHANGED
|
@@ -10,6 +10,9 @@ import { execFileSync } from 'node:child_process';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { canonicalRvfStores, hasCanonicalRvfStore } from './rvf-generation.mjs';
|
|
12
12
|
import { getVersionTag, stripTag } from './version.mjs';
|
|
13
|
+
// The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
|
|
14
|
+
// sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
|
|
15
|
+
import { orgRepoCount } from './org-repo-count.mjs';
|
|
13
16
|
|
|
14
17
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
18
|
const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
|
|
@@ -70,11 +73,12 @@ for (const store of canonicalRvfStores(KB_DIR)) {
|
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
const built = repos.filter((r) => r.status === 'built');
|
|
76
|
+
const ORG = orgRepoCount();
|
|
73
77
|
const manifest = {
|
|
74
78
|
brainVersion: stripTag(BRAIN_VERSION), // FIELD = bare literal; the "v" tag is display-only (see version.mjs)
|
|
75
79
|
generated: NOW,
|
|
76
80
|
generatedHuman: new Date(NOW).toUTCString(),
|
|
77
|
-
coverage: { built: built.length, catalogued: repos.length, orgTotalApprox:
|
|
81
|
+
coverage: { built: built.length, catalogued: repos.length, orgTotalApprox: ORG.count, orgTotalSource: ORG.source, orgTotalAt: ORG.at, pending: repos.length - built.length },
|
|
78
82
|
builtRepos: built,
|
|
79
83
|
pendingRepos: repos.filter((r) => r.status === 'pending').map((r) => ({ name: r.name, tier: r.tier })),
|
|
80
84
|
};
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -19,6 +19,10 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { getVersion, getVersionTag, stripTag } from './version.mjs';
|
|
21
21
|
import { auditRvfIndexes } from './rvf-index-audit.mjs';
|
|
22
|
+
import { validateSelectedRvfGenerations } from './rvf-generation.mjs';
|
|
23
|
+
// The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
|
|
24
|
+
// sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
|
|
25
|
+
import { orgRepoCount } from './org-repo-count.mjs';
|
|
22
26
|
|
|
23
27
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
28
|
const KB = path.join(ROOT, 'kb');
|
|
@@ -159,6 +163,15 @@ if (built.length === 0) {
|
|
|
159
163
|
console.error('[build-bundle] FATAL: zero public RVF stores are eligible for release. Refusing to publish an empty brain bundle.');
|
|
160
164
|
process.exit(1);
|
|
161
165
|
}
|
|
166
|
+
const generationValidation = validateSelectedRvfGenerations(ASSETS, {
|
|
167
|
+
selectedStores: built,
|
|
168
|
+
privateStores: [...PRIVATE_STORES],
|
|
169
|
+
});
|
|
170
|
+
if (generationValidation.failures.length) {
|
|
171
|
+
console.error('[build-bundle] FATAL: RVF generation ledger does not exactly bind selected roots:');
|
|
172
|
+
for (const failure of generationValidation.failures) console.error(` ${failure}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
162
175
|
const rvfIndexAudit = await auditRvfIndexes(
|
|
163
176
|
built.map((name) => path.join(ASSETS, `${name}.big.rvf`)),
|
|
164
177
|
);
|
|
@@ -205,7 +218,7 @@ for (const name of built) {
|
|
|
205
218
|
if (!fs.existsSync(generationsFile)) {
|
|
206
219
|
missing.push('RVF-GENERATIONS.json');
|
|
207
220
|
} else {
|
|
208
|
-
const source =
|
|
221
|
+
const source = generationValidation.manifest;
|
|
209
222
|
const stores = {};
|
|
210
223
|
for (const { name } of builtRepos) {
|
|
211
224
|
if (!source.stores?.[name]) missing.push(`RVF-GENERATIONS.json:${name}`);
|
|
@@ -213,8 +226,8 @@ for (const name of built) {
|
|
|
213
226
|
}
|
|
214
227
|
fs.writeFileSync(path.join(OUT, 'RVF-GENERATIONS.json'), `${JSON.stringify({
|
|
215
228
|
schemaVersion: source.schemaVersion,
|
|
216
|
-
brainVersion:
|
|
217
|
-
releaseTag:
|
|
229
|
+
brainVersion: stripTag(BRAIN_VERSION),
|
|
230
|
+
releaseTag: BRAIN_VERSION,
|
|
218
231
|
stores,
|
|
219
232
|
}, null, 2)}\n`);
|
|
220
233
|
copied++;
|
|
@@ -263,6 +276,26 @@ if (hasGists) for (const suf of ['ruv-gists.big.rvf', 'ruv-gists.big.rvf.idmap.j
|
|
|
263
276
|
fs.writeFileSync(path.join(OUT, 'capability-cards.md'), parts[0] + kept.join(''));
|
|
264
277
|
copied++;
|
|
265
278
|
if (filteredAny) console.log('[build-bundle] filtered private repo card(s) out of shipped capability-cards.md');
|
|
279
|
+
|
|
280
|
+
// repo-aliases.json TRAVELS WITH THE CARDS OR EVERY ALIAS IS DEAD ON ARRIVAL.
|
|
281
|
+
//
|
|
282
|
+
// card-lane.mjs reads it (REPO_ALIASES_FILE) to resolve a PRODUCT name to its installed store:
|
|
283
|
+
// `agent-harness-generator -> metaharness`, `ruvnet-brain -> brain`, `ruvector -> rvf`,
|
|
284
|
+
// `agentic-qe -> qe`. The bundle never copied it, so on every install those mappings were absent
|
|
285
|
+
// and the fast lane could not route them. Measured 2026-08-19 against the live store root:
|
|
286
|
+
// repo-aliases.json ABSENT, and `search_ruvnet("metaharness: what does Darwin mode mutate?")`
|
|
287
|
+
// answered "card router was ambiguous" — the product cannot be found by its own name.
|
|
288
|
+
//
|
|
289
|
+
// `forge-update.mjs:505` already lists this file among the ones it manages, so the updater knew
|
|
290
|
+
// about it and the builder did not — one fact, two shipping paths, only one of them told. Same
|
|
291
|
+
// shape as the reader/writer store-root split and the ingest `--out` bug fixed this week.
|
|
292
|
+
const aliasesSrc = path.join(KB, 'repo-aliases.json');
|
|
293
|
+
if (fs.existsSync(aliasesSrc)) {
|
|
294
|
+
fs.copyFileSync(aliasesSrc, path.join(OUT, 'repo-aliases.json'));
|
|
295
|
+
copied++;
|
|
296
|
+
} else {
|
|
297
|
+
console.log('[build-bundle] note: kb/repo-aliases.json absent — product-name aliases will not resolve on install');
|
|
298
|
+
}
|
|
266
299
|
} else {
|
|
267
300
|
console.log('[build-bundle] note: kb/capability-cards.md absent — the fast lane ships with no card source and will honestly fall through on every query');
|
|
268
301
|
}
|
|
@@ -395,11 +428,12 @@ cp(path.join(ROOT, 'keys', 'ruvnet-brain-signing.pub.pem'), path.join(OUT, 'keys
|
|
|
395
428
|
const builtLower = new Set(built.map((b) => b.toLowerCase()));
|
|
396
429
|
const pendingRepos = regFlat.filter((r) => !builtLower.has(r.name.toLowerCase())).map((r) => ({ name: r.name, tier: r.tier }));
|
|
397
430
|
const now = new Date();
|
|
431
|
+
const ORG = orgRepoCount();
|
|
398
432
|
const manifest = {
|
|
399
433
|
brainVersion: stripTag(BRAIN_VERSION), // FIELD = bare literal; BRAIN_VERSION stays the v-prefixed Release tag
|
|
400
434
|
generated: now.toISOString(),
|
|
401
435
|
generatedHuman: now.toUTCString(),
|
|
402
|
-
coverage: { built: builtRepos.length, catalogued: regFlat.length, orgTotalApprox:
|
|
436
|
+
coverage: { built: builtRepos.length, catalogued: regFlat.length, orgTotalApprox: ORG.count, orgTotalSource: ORG.source, orgTotalAt: ORG.at, pending: pendingRepos.length },
|
|
403
437
|
crossRepoTool: { mcp: 'forge-mcp-all.mjs', cli: 'forge-ask-all.mjs', tool: 'search_ruvnet' },
|
|
404
438
|
conceptsStore: hasConcepts ? { store: 'concepts.big.rvf', note: 'L2 synthesis + per-repo primers embedded as prose; unioned by search_ruvnet so code-implemented capabilities are retrievable as high-confidence prose.' } : null,
|
|
405
439
|
builtRepos,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* card-from-source.mjs — give a DARK store a card written from the repo's own words.
|
|
4
|
+
*
|
|
5
|
+
* A store with no capability card is DARK: the bytes are valid and no by-description query can
|
|
6
|
+
* reach them. `ingest-repo.mjs` says so on every run. After the 2026-08-20 bulk ingest the brain
|
|
7
|
+
* held ~111 stores and ~71 of them were dark — loaded, and unusable for anything except a query
|
|
8
|
+
* that already knew the repo's name, which is precisely the query a knowledge base is least needed
|
|
9
|
+
* for.
|
|
10
|
+
*
|
|
11
|
+
* THE LINE THIS DOES NOT CROSS. `ingest-new-repos.mjs` deliberately refuses to write cards, because
|
|
12
|
+
* a card invented from a repo NAME is a confident claim in the routing layer that nobody grounded —
|
|
13
|
+
* it routes real questions to a corpus that cannot answer them, which is worse than an honest gap.
|
|
14
|
+
* That refusal stands. This is a different act: it copies the repo's OWN description and the
|
|
15
|
+
* opening of its OWN README. Every content word in the card came from the repository. Nothing here
|
|
16
|
+
* infers what a project is "probably" for.
|
|
17
|
+
*
|
|
18
|
+
* AND IT SAYS WHAT IT IS. Each generated card carries a marker so a reader can tell it apart from
|
|
19
|
+
* the hand-written ones, which are richer — they say when to REACH for a tool and what it is NOT.
|
|
20
|
+
* An auto-derived card is a floor, not a substitute: it makes a store reachable and invites a
|
|
21
|
+
* better card later. Claiming otherwise would be the overselling this file exists to avoid.
|
|
22
|
+
*
|
|
23
|
+
* node scripts/card-from-source.mjs # report which stores are dark
|
|
24
|
+
* node scripts/card-from-source.mjs --apply # write cards for them
|
|
25
|
+
* node scripts/card-from-source.mjs --apply --max 20
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { storeRoot, darkStores } from '../kb/store-root.mjs';
|
|
32
|
+
|
|
33
|
+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
34
|
+
const OWNER = process.env.RUVNET_ORG_OWNER || 'ruvnet';
|
|
35
|
+
const APPLY = process.argv.includes('--apply');
|
|
36
|
+
const arg = (n, d) => { const i = process.argv.indexOf(n); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
|
|
37
|
+
const MAX = Number(arg('--max', '500'));
|
|
38
|
+
export const MARKER = 'Auto-derived from the repository\'s own description and README';
|
|
39
|
+
|
|
40
|
+
/** The repo's own description + README opening. Never a guess about what the name implies. */
|
|
41
|
+
export function sourceFacts(repo) {
|
|
42
|
+
let description = '';
|
|
43
|
+
let readme = '';
|
|
44
|
+
try {
|
|
45
|
+
const meta = JSON.parse(execFileSync('gh', ['api', `repos/${OWNER}/${repo}`], { encoding: 'utf8', maxBuffer: 1 << 24 }));
|
|
46
|
+
description = String(meta?.description || '').trim();
|
|
47
|
+
} catch { /* a repo can legitimately have no description */ }
|
|
48
|
+
try {
|
|
49
|
+
const raw = execFileSync('gh', ['api', `repos/${OWNER}/${repo}/readme`, '-H', 'Accept: application/vnd.github.raw'],
|
|
50
|
+
{ encoding: 'utf8', maxBuffer: 1 << 24 });
|
|
51
|
+
readme = raw;
|
|
52
|
+
} catch { /* and no README */ }
|
|
53
|
+
return { description, readme };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** First real prose of a README: skip badges, HTML, headings, and link-only lines. */
|
|
57
|
+
export function firstProse(readme, limit = 600) {
|
|
58
|
+
const lines = String(readme || '').split('\n');
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const t = line.trim();
|
|
62
|
+
if (!t) { if (out.length) break; continue; }
|
|
63
|
+
if (t.startsWith('#') || t.startsWith('<') || t.startsWith('|') || t.startsWith('---')) continue;
|
|
64
|
+
if (/^!?\[[^\]]*\]\([^)]*\)$/.test(t)) continue; // a lone badge or image
|
|
65
|
+
if (/^\[!\[/.test(t)) continue; // badge-wrapped link
|
|
66
|
+
out.push(t);
|
|
67
|
+
if (out.join(' ').length > limit) break;
|
|
68
|
+
}
|
|
69
|
+
return out.join(' ').replace(/\s+/g, ' ').slice(0, limit).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildCard(repo, { description, readme }) {
|
|
73
|
+
const prose = firstProse(readme);
|
|
74
|
+
if (!description && !prose) return null; // nothing grounded to say — leave it dark, honestly
|
|
75
|
+
const body = [description, prose].filter(Boolean).join(' — ');
|
|
76
|
+
return `## ${repo.toLowerCase()}\n${body}\n(${MARKER}; a hand-written card saying when to reach for it, and when not to, would be better.)\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function insertSorted(file, card, name) {
|
|
80
|
+
const s = fs.readFileSync(file, 'utf8');
|
|
81
|
+
if (new RegExp(`^## ${name}$`, 'mi').test(s)) return false;
|
|
82
|
+
const heads = [...s.matchAll(/^## (.+)$/gm)].map((m) => [m.index, m[1].trim().toLowerCase()]);
|
|
83
|
+
const pos = heads.find(([, n]) => n > name)?.[0];
|
|
84
|
+
fs.writeFileSync(file, pos === undefined ? `${s.replace(/\n+$/, '')}\n\n${card}` : s.slice(0, pos) + card + '\n' + s.slice(pos));
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isMain = (() => {
|
|
89
|
+
try { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); }
|
|
90
|
+
catch { return false; }
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
if (isMain) {
|
|
94
|
+
const root = storeRoot();
|
|
95
|
+
const dark = darkStores(root);
|
|
96
|
+
console.log(`[card] ${dark.length} dark store(s) — valid bytes no by-description query can reach`);
|
|
97
|
+
if (!dark.length) process.exit(0);
|
|
98
|
+
if (!APPLY) {
|
|
99
|
+
console.log(`[card] ${dark.slice(0, 20).join(', ')}${dark.length > 20 ? ' …' : ''}`);
|
|
100
|
+
console.log('[card] report only. Re-run with --apply to write cards from each repo\'s own source.');
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
const repoCards = path.join(ROOT, 'kb', 'capability-cards.md');
|
|
104
|
+
const liveCards = path.join(root, 'capability-cards.md');
|
|
105
|
+
let wrote = 0; let skipped = 0;
|
|
106
|
+
for (const name of dark.slice(0, MAX)) {
|
|
107
|
+
const card = buildCard(name, sourceFacts(name));
|
|
108
|
+
if (!card) { skipped += 1; console.log(`[card] ${name}: no description and no README prose — left dark rather than invented`); continue; }
|
|
109
|
+
for (const f of [repoCards, liveCards]) { try { insertSorted(f, card, name.toLowerCase()); } catch { /* keep going */ } }
|
|
110
|
+
wrote += 1;
|
|
111
|
+
}
|
|
112
|
+
const remaining = darkStores(root).length;
|
|
113
|
+
console.log(`\n[card] wrote ${wrote}, left ${skipped} dark for lack of grounded source text, ${remaining} still dark.`);
|
|
114
|
+
}
|
|
@@ -126,7 +126,7 @@ export function buildHealthRecommendations({ memory = null, learning = null } =
|
|
|
126
126
|
if (Number.isFinite(age) && age > STALE_TRAIN_SECONDS) {
|
|
127
127
|
recs.push(makeRecommendation({
|
|
128
128
|
id: 'learning:train',
|
|
129
|
-
scope: '
|
|
129
|
+
scope: 'project',
|
|
130
130
|
title: 'Your learner has gone quiet',
|
|
131
131
|
rationale: 'It is installed and switched on, but it has not learned anything recently — so it is not getting smarter.',
|
|
132
132
|
severity: 'SUGGESTED',
|