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
|
@@ -119,3 +119,47 @@ export function verifyRvfGenerations(dir, {
|
|
|
119
119
|
}
|
|
120
120
|
return { manifest, failures };
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
// Release-time closure check for the exact public roots selected for a bundle. Unlike the
|
|
124
|
+
// source-only verifier above, this rejects every ambiguity in the byte-bearing asset directory.
|
|
125
|
+
export function validateSelectedRvfGenerations(dir, {
|
|
126
|
+
selectedStores = canonicalRvfStores(dir),
|
|
127
|
+
privateStores = [],
|
|
128
|
+
version,
|
|
129
|
+
releaseTag,
|
|
130
|
+
} = {}) {
|
|
131
|
+
const manifest = readRvfGenerations(dir);
|
|
132
|
+
const failures = [];
|
|
133
|
+
const selected = [...new Set(selectedStores)].sort();
|
|
134
|
+
const selectedLower = new Map(selected.map((name) => [name.toLowerCase(), name]));
|
|
135
|
+
const privateLower = new Set(privateStores.map((name) => String(name).toLowerCase()));
|
|
136
|
+
if (version !== undefined && manifest.brainVersion !== version) failures.push(`brainVersion=${manifest.brainVersion}, expected ${version}`);
|
|
137
|
+
if (releaseTag !== undefined && manifest.releaseTag !== releaseTag) failures.push(`releaseTag=${manifest.releaseTag}, expected ${releaseTag}`);
|
|
138
|
+
for (const name of selected) if (privateLower.has(name.toLowerCase())) failures.push(`${name}: private store selected`);
|
|
139
|
+
for (const [store, row] of Object.entries(manifest.stores || {})) {
|
|
140
|
+
const canonical = selectedLower.get(store.toLowerCase());
|
|
141
|
+
if (!canonical) {
|
|
142
|
+
if (!privateLower.has(store.toLowerCase())) failures.push(`${store}: extra generation record`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (canonical !== store) failures.push(`${store}: alias differs from selected store ${canonical}`);
|
|
146
|
+
const expectedFile = `${canonical}.big.rvf`;
|
|
147
|
+
if (row?.file !== expectedFile) failures.push(`${store}: file=${row?.file}, expected ${expectedFile}`);
|
|
148
|
+
const file = path.join(dir, expectedFile);
|
|
149
|
+
if (!fs.existsSync(file)) failures.push(`${store}: missing ${expectedFile}`);
|
|
150
|
+
else {
|
|
151
|
+
const stat = fs.statSync(file);
|
|
152
|
+
if (row?.bytes !== stat.size) failures.push(`${store}: bytes=${row?.bytes}, actual ${stat.size}`);
|
|
153
|
+
const digest = sha256File(file);
|
|
154
|
+
if (row?.sha256 !== digest) failures.push(`${store}: sha256=${digest}, recorded ${row?.sha256}`);
|
|
155
|
+
}
|
|
156
|
+
if (typeof row?.model !== 'string' || !row.model.trim()) failures.push(`${store}: invalid model`);
|
|
157
|
+
if (!Number.isInteger(row?.dimensions) || row.dimensions <= 0) failures.push(`${store}: invalid dimensions`);
|
|
158
|
+
if (row?.sourceCommit !== null && (typeof row?.sourceCommit !== 'string' || !/^[a-f0-9]{7,64}$/i.test(row.sourceCommit))) failures.push(`${store}: invalid sourceCommit`);
|
|
159
|
+
if (typeof row?.builtUtc !== 'string' || !Number.isFinite(Date.parse(row.builtUtc))) failures.push(`${store}: invalid builtUtc`);
|
|
160
|
+
}
|
|
161
|
+
for (const name of selected) if (!manifest.stores?.[name]) failures.push(`${name}: no exact generation record`);
|
|
162
|
+
const publicRows = Object.keys(manifest.stores || {}).filter((name) => !privateLower.has(name.toLowerCase()));
|
|
163
|
+
if (publicRows.length !== selected.length) failures.push(`store count=${publicRows.length}, selected ${selected.length}`);
|
|
164
|
+
return { manifest, selectedStores: selected, failures };
|
|
165
|
+
}
|
package/scripts/selfcheck.mjs
CHANGED
|
@@ -532,7 +532,15 @@ export async function runBattery({ home = os.homedir(), repo = null, cwd = os.tm
|
|
|
532
532
|
export async function checkCoexistence({ home = os.homedir(), repo = null } = {}) {
|
|
533
533
|
const reg = await loadRegistry();
|
|
534
534
|
const registry = reg.buildRegistry({ repo: repo ?? reg.REPO, home, includeMachine: true });
|
|
535
|
-
|
|
535
|
+
// 'codex' joined hook-registry.mjs's mesh 2026-08-20 (Dream Cycle cross-host-conformance) — it is
|
|
536
|
+
// OURS (plugin/hooks/codex-hooks.json, shipped in package.json's "files" tree same as plugin/),
|
|
537
|
+
// never a foreign registration. Omitting it here silently undercounted `ourCount` and dropped all
|
|
538
|
+
// 16 Codex registrations from both buckets — caught by an independent critique before this shipped.
|
|
539
|
+
// 'codex' joined hook-registry.mjs's mesh 2026-08-20 (Dream Cycle cross-host-conformance) — it is
|
|
540
|
+
// OURS (plugin/hooks/codex-hooks.json, shipped in package.json's "files" tree same as plugin/),
|
|
541
|
+
// never a foreign registration. Omitting it here silently undercounted `ourCount` and dropped all
|
|
542
|
+
// 16 Codex registrations from both buckets — caught by an independent critique before this shipped.
|
|
543
|
+
const ours = registry.records.filter((r) => r.layer === 'plugin' || r.layer === 'codex' || r.layer === 'plugin-installed' || r.layer === 'marketplace-clone');
|
|
536
544
|
const foreign = registry.records.filter((r) => r.layer === 'user' || r.layer.startsWith('third-party:') || r.layer === 'project');
|
|
537
545
|
|
|
538
546
|
// DOUBLE REGISTRATION — reused wholesale from hook-registry (ADR-055 M1): one handler, an
|
|
@@ -52,7 +52,12 @@ export function createStabilizationReceipt({ root, artifactPath, qe, audit }) {
|
|
|
52
52
|
scoreClaimed: false,
|
|
53
53
|
sha,
|
|
54
54
|
tree: git('rev-parse', 'HEAD^{tree}'),
|
|
55
|
+
// NAME WHAT IS DIRTY, or the failure is unactionable. `release-qe` has failed on EVERY main run
|
|
56
|
+
// with a bare `INVALID_LINEAGE`, which says a clean tree is required and not one word about what
|
|
57
|
+
// made it unclean — so the release chain has been blocked with no way to see why from the log.
|
|
58
|
+
// The receipt now carries the offending paths; the seal is unchanged, only its evidence is.
|
|
55
59
|
dirty: Boolean(git('status', '--porcelain')),
|
|
60
|
+
dirtyPaths: String(git('status', '--porcelain') || '').split('\n').filter(Boolean).slice(0, 40),
|
|
56
61
|
version: manifest.version,
|
|
57
62
|
tag: `v${manifest.version}`,
|
|
58
63
|
sourceVersions: { package: manifest.version, claudePlugin: claude.version, codexPlugin: codex.version },
|
|
@@ -75,7 +80,12 @@ export function createStabilizationReceipt({ root, artifactPath, qe, audit }) {
|
|
|
75
80
|
],
|
|
76
81
|
};
|
|
77
82
|
const result = evaluateStabilizationCandidateReceipt(receipt);
|
|
78
|
-
if (result.verdict !== 'PASS')
|
|
83
|
+
if (result.verdict !== 'PASS') {
|
|
84
|
+
const detail = receipt.dirty && receipt.dirtyPaths?.length
|
|
85
|
+
? `\n working tree is dirty (${receipt.dirtyPaths.length} path(s)):\n ${receipt.dirtyPaths.join('\n ')}`
|
|
86
|
+
: '';
|
|
87
|
+
throw new Error(`stabilization seal failed: ${result.failures.map(({ code }) => code).join(',')}${detail}`);
|
|
88
|
+
}
|
|
79
89
|
return receipt;
|
|
80
90
|
}
|
|
81
91
|
|
|
Binary file
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sync-commands.mjs — four spellings of one command share ONE body, generated, never hand-copied.
|
|
4
|
+
*
|
|
5
|
+
* ISSUE #135. `/rvbc`, `/rvcb`, `/brain-console` and `/ruvnet-brain:configure` each declare, in their
|
|
6
|
+
* own words, that every spelling is equally valid and the user must never be corrected. Their BODIES
|
|
7
|
+
* had drifted into four independent hand-written specs, measured on 4.0.36:
|
|
8
|
+
*
|
|
9
|
+
* rvbc.md 4116 B rvcb.md 976 B brain-console.md 985 B configure.md 1680 B
|
|
10
|
+
*
|
|
11
|
+
* Four rules lived only in `rvbc.md`: speak first before any tool call; the "Things NOT to do before
|
|
12
|
+
* the page is open" list; the emphatic `run_in_background: true, ALWAYS` *with its reason*; and
|
|
13
|
+
* "already running is success, not an error." So which spelling a user happened to type changed how
|
|
14
|
+
* the assistant behaved — on the one command whose entire promise is that spelling does not matter.
|
|
15
|
+
*
|
|
16
|
+
* Two of those cost real silence. Without the "no knowledge search first" prohibition, grounding
|
|
17
|
+
* before opening the page is a legal move and a cold embedding-model load is allowed up to 180
|
|
18
|
+
* seconds. And `rvcb.md` kept the `run_in_background` RULE but dropped the REASON — that a foreground
|
|
19
|
+
* run hangs the call until the 120-second timeout — and a rule whose reason is missing is a rule that
|
|
20
|
+
* gets traded away.
|
|
21
|
+
*
|
|
22
|
+
* THE FIX IS NOT "COPY THE FOUR RULES ACROSS." That is what produced this: four copies of one fact,
|
|
23
|
+
* drifting the moment anyone edits one. It is ADR-065's rule applied to prompts instead of values —
|
|
24
|
+
* a fact that appears in more than one place gets exactly ONE producer, and every other occurrence is
|
|
25
|
+
* written from it. `rvbc.md` is the producer; the other three are generated.
|
|
26
|
+
*
|
|
27
|
+
* WHAT EACH ALIAS KEEPS: its own `description:`. That is not duplicated knowledge — it is what the
|
|
28
|
+
* user reads in the command picker, and each spelling legitimately introduces itself differently.
|
|
29
|
+
* Everything below the frontmatter is byte-identical by construction.
|
|
30
|
+
*
|
|
31
|
+
* node scripts/sync-commands.mjs # rewrite the aliases from the canonical body
|
|
32
|
+
* node scripts/sync-commands.mjs --check # fail if any alias has drifted (CI / pre-push)
|
|
33
|
+
*/
|
|
34
|
+
import fs from 'node:fs';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
|
|
38
|
+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
39
|
+
const DIR = path.join(ROOT, 'plugin', 'commands');
|
|
40
|
+
const CANONICAL = 'rvbc.md';
|
|
41
|
+
const ALIASES = ['rvcb.md', 'brain-console.md', 'configure.md'];
|
|
42
|
+
const CHECK = process.argv.includes('--check');
|
|
43
|
+
|
|
44
|
+
/** Split `---\n…\n---\n` frontmatter from the body. Both are returned verbatim. */
|
|
45
|
+
export function splitFrontmatter(text) {
|
|
46
|
+
const m = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
47
|
+
if (!m) return { frontmatter: null, body: text };
|
|
48
|
+
return { frontmatter: m[1], body: text.slice(m[0].length) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The alias's own description line, kept; everything else in the frontmatter comes from canonical. */
|
|
52
|
+
function rebuild(aliasText, canonicalFrontmatter, canonicalBody) {
|
|
53
|
+
const { frontmatter } = splitFrontmatter(aliasText);
|
|
54
|
+
const desc = /^description:.*(?:\n[ \t]+.*)*$/m.exec(frontmatter || '');
|
|
55
|
+
const rest = canonicalFrontmatter
|
|
56
|
+
.split('\n')
|
|
57
|
+
.filter((l) => !/^description:/.test(l))
|
|
58
|
+
.join('\n');
|
|
59
|
+
const head = [desc ? desc[0] : null, rest].filter(Boolean).join('\n');
|
|
60
|
+
return `---\n${head}\n---\n${canonicalBody}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// RUN THE CLI ONLY WHEN INVOKED AS ONE. Without this guard, `import { splitFrontmatter }` from a test
|
|
64
|
+
// executed the whole generator — measured: importing this file printed "[commands] synced 0 alias(es)"
|
|
65
|
+
// inside the test runner, and had vitest's argv contained `--check` it would have called process.exit
|
|
66
|
+
// and taken the runner down with it. The identical defect was fixed in tests/unit/nightly-convergence
|
|
67
|
+
// one commit earlier (importing bin/install.mjs ran the installer main); it recurred here because that
|
|
68
|
+
// fix was applied to the CALLER instead of to the module. Fixing it at the module is what makes it
|
|
69
|
+
// stop recurring — same reasoning as ADR-066: one producer, not a rule every caller must remember.
|
|
70
|
+
// realpathSync THROWS on a path that does not resolve, and under vitest on Windows `process.argv[1]`
|
|
71
|
+
// is not a real file — measured in CI as `ENOENT: lstat 'D:\D:'`, which failed the whole SUITE at
|
|
72
|
+
// import time rather than any assertion. A guard that decides "am I the entrypoint" must never be
|
|
73
|
+
// able to crash the caller that merely imported this module: not-resolvable means not-main.
|
|
74
|
+
function resolvedIsMain() {
|
|
75
|
+
try {
|
|
76
|
+
return Boolean(process.argv[1])
|
|
77
|
+
&& fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
78
|
+
} catch { return false; }
|
|
79
|
+
}
|
|
80
|
+
if (!resolvedIsMain()) { /* imported for its exports — do nothing else */ } else {
|
|
81
|
+
|
|
82
|
+
const canonicalText = fs.readFileSync(path.join(DIR, CANONICAL), 'utf8');
|
|
83
|
+
const { frontmatter: canonicalFrontmatter, body: canonicalBody } = splitFrontmatter(canonicalText);
|
|
84
|
+
if (!canonicalFrontmatter) {
|
|
85
|
+
process.stderr.write(`[commands] ${CANONICAL} has no frontmatter — refusing to generate from it\n`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const drift = [];
|
|
90
|
+
let wrote = 0;
|
|
91
|
+
for (const alias of ALIASES) {
|
|
92
|
+
const file = path.join(DIR, alias);
|
|
93
|
+
if (!fs.existsSync(file)) { drift.push(`${alias} (missing)`); continue; }
|
|
94
|
+
const before = fs.readFileSync(file, 'utf8');
|
|
95
|
+
const after = rebuild(before, canonicalFrontmatter, canonicalBody);
|
|
96
|
+
if (after === before) continue;
|
|
97
|
+
if (CHECK) { drift.push(alias); continue; }
|
|
98
|
+
fs.writeFileSync(file, after);
|
|
99
|
+
process.stdout.write(`[commands] ${alias}: body regenerated from ${CANONICAL}\n`);
|
|
100
|
+
wrote += 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (CHECK) {
|
|
104
|
+
if (drift.length) {
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
`[commands] DRIFT: ${drift.length} alias(es) disagree with ${CANONICAL}. A user who types one\n`
|
|
107
|
+
+ ' spelling would get different behaviour from one who types another, on the command\n'
|
|
108
|
+
+ ' that promises spelling does not matter. Run: node scripts/sync-commands.mjs\n',
|
|
109
|
+
);
|
|
110
|
+
for (const d of drift) process.stderr.write(` ${d}\n`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
process.stdout.write(`[commands] all ${ALIASES.length} alias(es) share ${CANONICAL}'s body\n`);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
process.stdout.write(`[commands] synced ${wrote} alias(es) to ${CANONICAL}\n`);
|
|
117
|
+
}
|