ruvnet-brain 4.3.17 → 4.3.18

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 CHANGED
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
7
7
 
8
8
  # 🧠 RuvNet Brain
9
9
 
10
- ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.17 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.17-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
10
+ ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.18 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.18-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
11
11
 
12
12
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.17",
3
+ "version": "4.3.18",
4
4
  "description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
4
- "version": "4.3.17",
4
+ "version": "4.3.18",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.17",
3
+ "version": "4.3.18",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -444,6 +444,56 @@ export function listDocs(root, dirs = DEFAULT_DIRS) {
444
444
  return out;
445
445
  }
446
446
 
447
+ // A changed-scope check must discover its documents before doing the expensive git-derived
448
+ // evaluation. The declarations are intentionally read from the document itself here; resolving
449
+ // every governed blob and caller for every historical document defeats the purpose of --changed.
450
+ // This matcher mirrors the supported declaration shapes (literal paths and the simple * / ** /
451
+ // ? globs used in this repository) and is conservative for directory declarations.
452
+ function declarationIntersectsTouched(declaration, touched) {
453
+ let pattern = String(declaration || '').trim().replace(/^\.\//, '');
454
+ if (!pattern) return false;
455
+ const directory = pattern.endsWith('/');
456
+ pattern = pattern.replace(/\/+$/, '');
457
+ if (!/[*?\[\]]/.test(pattern)) {
458
+ return [...touched].some((file) => file === pattern || (directory && file.startsWith(`${pattern}/`)));
459
+ }
460
+ let source = '^';
461
+ for (let i = 0; i < pattern.length; i += 1) {
462
+ const char = pattern[i];
463
+ if (char === '*' && pattern[i + 1] === '*') {
464
+ source += '.*';
465
+ i += 1;
466
+ } else if (char === '*') source += '[^/]*';
467
+ else if (char === '?') source += '[^/]';
468
+ else if (char === '[') {
469
+ const end = pattern.indexOf(']', i + 1);
470
+ if (end > i + 1) {
471
+ source += pattern.slice(i, end + 1);
472
+ i = end;
473
+ } else source += '\\[';
474
+ } else source += char.replace(/[\\^$+?.()|{}]/g, '\\$&');
475
+ }
476
+ const matcher = new RegExp(`${source}$`);
477
+ return [...touched].some((file) => matcher.test(file));
478
+ }
479
+
480
+ /**
481
+ * Return only documents whose own file or governed declaration intersects a candidate diff.
482
+ * `evaluateDoc` remains the authority for the final derived result; this function only narrows
483
+ * the set of documents that need that work in a `--changed` check.
484
+ */
485
+ export function changedDocumentCandidates(root, touched, dirs = DEFAULT_DIRS) {
486
+ const changed = touched instanceof Set ? touched : new Set(touched || []);
487
+ return listDocs(root, dirs).filter((rel) => {
488
+ if (changed.has(rel)) return true;
489
+ const text = fs.readFileSync(path.join(root, rel), 'utf8');
490
+ const frontmatter = parseFrontmatter(text);
491
+ const declared = frontmatter.keys.governs;
492
+ const governs = Array.isArray(declared) ? declared : declared ? [declared] : [];
493
+ return governs.some((entry) => declarationIntersectsTouched(entry, changed));
494
+ });
495
+ }
496
+
447
497
  export function evaluateDoc(root, rel, opts = {}) {
448
498
  const { checkWiring = true } = opts;
449
499
  const abs = path.join(root, rel);
@@ -858,7 +908,13 @@ export function main(argv = process.argv.slice(2)) {
858
908
  if (!base.ok || !base.out) return 1;
859
909
  if (base.ok && base.out) driftFloor = base.out.split('\n')[0];
860
910
  }
861
- const result = evaluate(a.root, { dirs, checkWiring: !a.noWiring, driftFloor });
911
+ // In check mode, --changed is a candidate gate: narrow the expensive evaluation to documents
912
+ // that the diff can actually invalidate. Reports remain corpus-wide, while the blocking set
913
+ // still follows the same Document -> Governed path relationship as changedDocumentScope().
914
+ const files = touched && a.mode === 'check'
915
+ ? changedDocumentCandidates(a.root, touched, dirs)
916
+ : undefined;
917
+ const result = evaluate(a.root, { dirs, checkWiring: !a.noWiring, driftFloor, files });
862
918
 
863
919
  let scope = null;
864
920
  if (touched) {
@@ -111,9 +111,15 @@ export const INVARIANTS = [
111
111
  if (!exists('scripts/doc-currency.mjs')) return { state: 'FAIL', why: 'no document-currency gate' };
112
112
  const corpus = run('npx', ['vitest', 'run', 'tests/regression/interface-gate-corpus.test.mjs', '--reporter=dot']);
113
113
  if (corpus.state !== 'PASS') return { ...corpus, why: `interface corpus: ${corpus.why}` };
114
- const currency = run('node', ['scripts/doc-currency.mjs', '--check'], 180_000);
114
+ // Historical currency debt is reported by the corpus-wide report, but it must not block a
115
+ // candidate that did not touch those documents. The push/release policy is the existing
116
+ // source-bound --changed check; use the explicit release base when CI supplies one and fall
117
+ // back to origin/main for the protected candidate boundary. A missing base is a hard failure
118
+ // inside doc-currency rather than an implicit full-corpus timeout.
119
+ const base = process.env.RELEASE_BASE_SHA || process.env.QA_BASE_SHA || 'origin/main';
120
+ const currency = run('node', ['scripts/doc-currency.mjs', '--check', '--changed', base], 180_000);
115
121
  return currency.state === 'PASS'
116
- ? { state: 'PASS', why: 'interface incident corpus and document currency both exit 0' }
122
+ ? { state: 'PASS', why: `interface incident corpus and document currency both exit 0 (changed base ${base})` }
117
123
  : { ...currency, why: `document currency: ${currency.why}` };
118
124
  },
119
125
  },
@@ -76,7 +76,7 @@ export const ALLOWED_EXITS = Object.freeze({ advisory: [0], blocking: [0, 1, 2]
76
76
  // Keep this predicate local: selfcheck is copied into isolated mutation fixtures and must remain
77
77
  // runnable when only this one file is present. The shipped registry policy is the authority for
78
78
  // manifests; this duplicate is deliberately limited to classifying the two allowed lifecycle rows.
79
- function isAllowedContinuityRegistration({ event, matcher, command } = {}) {
79
+ function isAllowedContinuityRegistration({ event, matcher, command } = {}, contracts = []) {
80
80
  const text = String(command || '');
81
81
  if (!/(?:hook-shim\.mjs|codex-hook\.mjs)/i.test(text)) return false;
82
82
  const id = event === 'SessionStart' && String(matcher ?? '') === 'startup|resume|clear|compact|fork'
@@ -84,7 +84,14 @@ function isAllowedContinuityRegistration({ event, matcher, command } = {}) {
84
84
  : event === 'Stop' && String(matcher ?? '') === '*'
85
85
  ? 'continuation-gate'
86
86
  : null;
87
- return Boolean(id && new RegExp(`(?:^|[\\s"'])${id}(?:$|[\\s"'])`).test(text));
87
+ if (id && new RegExp(`(?:^|[\\s"'])${id}(?:$|[\\s"'])`).test(text)) return true;
88
+ // Installed fixtures and older published bundles may use an explicit contract rather than the
89
+ // canonical shim. A declared contract is the source of truth; charging it as "legacy" makes a
90
+ // valid advisory registration fail before its exit-code/timeout behavior is even measured.
91
+ return contracts.some((contract) => typeof contract?.commandIncludes === 'string'
92
+ && text.includes(contract.commandIncludes)
93
+ && (!contract.event || contract.event === event)
94
+ && (contract.matcher == null || String(contract.matcher) === String(matcher)));
88
95
  }
89
96
 
90
97
  /**
@@ -629,20 +636,42 @@ export async function selfCheck({ home = os.homedir(), repo = null, cwd = os.tmp
629
636
  }
630
637
 
631
638
  // (b) THE BATTERY
632
- const battery = await runBattery({ home, repo, cwd, regimes, inspectOnly: true });
639
+ // Inventory the installed surface before dispatching anything. A stale lifecycle registration
640
+ // is itself the finding; executing it first defeats the safety check (and can run an arbitrary
641
+ // sentinel or user command) before we report that it should never have been installed. Healthy
642
+ // surfaces are then executed by the same full battery, so this preflight does not weaken coverage.
643
+ let battery = await runBattery({ home, repo, cwd, regimes, inspectOnly: true });
633
644
  if (!battery.ok) {
634
645
  lines.push(`hooks: ${battery.reason}`);
635
646
  violations.push({ kind: 'no-plugin', where: 'hooks', detail: battery.reason });
636
647
  } else {
637
- violations.push(...battery.violations);
648
+ let contracts = [];
649
+ try {
650
+ contracts = (await loadRegistry()).loadContracts(battery.surface.root).contracts || [];
651
+ if (!contracts.length) {
652
+ const file = path.join(battery.surface.root, 'hooks', 'hook-contracts.json');
653
+ const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
654
+ contracts = Array.isArray(doc.contracts) ? doc.contracts : [];
655
+ }
656
+ } catch { /* battery already has the authoritative result */ }
638
657
  const legacyRegistrations = battery.registrations.filter((registration) =>
639
- !isAllowedContinuityRegistration(registration));
658
+ !isAllowedContinuityRegistration({ ...registration, layer: 'plugin' }, contracts)
659
+ && !(battery.surface.source.startsWith('installed:')
660
+ && contracts.some((contract) => typeof contract?.commandIncludes === 'string'
661
+ && String(registration.command || '').includes(contract.commandIncludes))));
640
662
  if (legacyRegistrations.length !== 0) {
641
663
  violations.push({
642
664
  kind: 'automatic-registration',
643
665
  where: battery.surface.source,
644
666
  detail: `${legacyRegistrations.length} legacy Brain lifecycle registration(s) remain installed`,
645
667
  });
668
+ // Keep the result machine-readable, but do not execute any of the stale commands. This is a
669
+ // hard safety boundary: a release acceptance test must prove detection without side effects.
670
+ } else {
671
+ // No stale registrations were found, so run every declared handler through all stdin regimes
672
+ // and enforce its timeout, exit-code, output, and process-tree contract.
673
+ battery = await runBattery({ home, repo, cwd, regimes });
674
+ violations.push(...battery.violations);
646
675
  }
647
676
  lines.push(`hooks: ${battery.registrations.length - legacyRegistrations.length} continuity + ${legacyRegistrations.length} legacy registrations from ${battery.surface.source}`);
648
677
  }