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.
Files changed (68) hide show
  1. package/README.md +28 -5
  2. package/bin/install.mjs +405 -23
  3. package/data/model-catalog.json +104 -15
  4. package/package.json +2 -1
  5. package/plugin/.claude-plugin/plugin.json +2 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/commands/brain-console.md +72 -9
  8. package/plugin/commands/configure.md +67 -21
  9. package/plugin/commands/rvcb.md +72 -9
  10. package/plugin/hooks/codex-hooks.json +40 -33
  11. package/plugin/hooks/hook-contracts.json +14 -24
  12. package/plugin/hooks/hooks.json +7 -42
  13. package/plugin/mcp/server.mjs +23 -6
  14. package/plugin/scripts/adr-currency-gate.mjs +150 -0
  15. package/plugin/scripts/capability-registry.mjs +10 -1
  16. package/plugin/scripts/codex-hook-adapter.mjs +121 -19
  17. package/plugin/scripts/codex-hook-wrapper.mjs +61 -4
  18. package/plugin/scripts/continuation-gate.mjs +148 -8
  19. package/plugin/scripts/decision-gate.mjs +428 -0
  20. package/plugin/scripts/decision-outcomes.mjs +231 -0
  21. package/plugin/scripts/degradation-watch.mjs +271 -0
  22. package/plugin/scripts/ground-ruvnet.sh +51 -11
  23. package/plugin/scripts/hijack-ruvnet.sh +11 -3
  24. package/plugin/scripts/hook-registry.mjs +48 -3
  25. package/plugin/scripts/hook-shim.mjs +58 -3
  26. package/plugin/scripts/identifier-preflight.mjs +134 -0
  27. package/plugin/scripts/learn-capture.sh +50 -1
  28. package/plugin/scripts/learn-flush.mjs +5 -5
  29. package/plugin/scripts/lesson-bridge.mjs +343 -0
  30. package/plugin/scripts/lesson-hooks.sh +26 -0
  31. package/plugin/scripts/lesson-promote.mjs +50 -0
  32. package/plugin/scripts/lesson-store.mjs +6 -1
  33. package/plugin/scripts/mcp-readiness.mjs +107 -0
  34. package/plugin/scripts/protect-brain-state.sh +9 -0
  35. package/plugin/scripts/runtime-preferences.mjs +40 -0
  36. package/plugin/scripts/session-snapshot-hook.mjs +15 -6
  37. package/plugin/scripts/spend-guard.mjs +125 -0
  38. package/plugin/scripts/unprompted-runtime.mjs +12 -2
  39. package/plugin/scripts/update-apply.mjs +7 -2
  40. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +20 -5
  41. package/plugin/skills/ruvnet-brain/SKILL.md +3 -3
  42. package/scripts/brain-score.mjs +261 -0
  43. package/scripts/brain-stamp.mjs +5 -1
  44. package/scripts/build-bundle.mjs +38 -4
  45. package/scripts/card-from-source.mjs +114 -0
  46. package/scripts/console-engine.mjs +1 -1
  47. package/scripts/corpus-candidate.mjs +294 -0
  48. package/scripts/corpus-reconcile.mjs +273 -0
  49. package/scripts/corpus-seed-publish.mjs +110 -0
  50. package/scripts/health-repair.mjs +11 -2
  51. package/scripts/ingest-new-repos.mjs +122 -0
  52. package/scripts/ingest-repo.mjs +66 -6
  53. package/scripts/learning-replay-cli.mjs +8 -3
  54. package/scripts/learning-replay-fixture.mjs +25 -6
  55. package/scripts/learning-replay-proof.mjs +38 -0
  56. package/scripts/nightly-wrapper.sh +55 -7
  57. package/scripts/onboarding-console.mjs +21 -1
  58. package/scripts/org-repo-count.mjs +119 -0
  59. package/scripts/rebuild-gists-from-receipts.mjs +246 -0
  60. package/scripts/release-transaction.mjs +30 -2
  61. package/scripts/release.mjs +147 -0
  62. package/scripts/repo-count-detector.mjs +62 -0
  63. package/scripts/restore-local-ingests.mjs +116 -0
  64. package/scripts/rvf-generation.mjs +44 -0
  65. package/scripts/selfcheck.mjs +9 -1
  66. package/scripts/stabilization-receipt.mjs +11 -1
  67. package/scripts/sync-census.mjs +0 -0
  68. 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
+ }
@@ -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
- const ours = registry.records.filter((r) => r.layer === 'plugin' || r.layer === 'plugin-installed' || r.layer === 'marketplace-clone');
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') throw new Error(`stabilization seal failed: ${result.failures.map(({ code }) => code).join(',')}`);
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
+ }