ruvnet-brain 4.0.12 → 4.0.28

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 (46) hide show
  1. package/README.md +5 -5
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +2 -2
  4. package/plugin/.codex-plugin/plugin.json +1 -1
  5. package/plugin/scripts/advocacy-outcomes.mjs +808 -0
  6. package/plugin/scripts/anticipate.sh +80 -14
  7. package/plugin/scripts/capability-registry.mjs +994 -0
  8. package/plugin/scripts/codex-hook-wrapper.mjs +1 -0
  9. package/plugin/scripts/continuation-gate.mjs +129 -1
  10. package/plugin/scripts/gates.mjs +146 -0
  11. package/plugin/scripts/goal-match.mjs +398 -0
  12. package/plugin/scripts/hijack-ruvnet.sh +69 -1
  13. package/plugin/scripts/hook-registry.mjs +616 -0
  14. package/plugin/scripts/hook-shim.mjs +13 -2
  15. package/plugin/scripts/learning-enable.mjs +382 -0
  16. package/plugin/scripts/lesson-promote.mjs +262 -0
  17. package/plugin/scripts/lesson-provenance.mjs +43 -0
  18. package/plugin/scripts/lesson-store.mjs +67 -56
  19. package/plugin/scripts/memory-doctor.mjs +345 -0
  20. package/plugin/scripts/nightly-controller.mjs +98 -0
  21. package/plugin/scripts/runtime-preferences.mjs +18 -0
  22. package/plugin/scripts/session-start-core.mjs +3 -3
  23. package/plugin/scripts/unprompted-runtime.mjs +22 -7
  24. package/plugin/scripts/user-settings.mjs +672 -0
  25. package/plugin/skills/ruvnet-brain/SKILL.md +2 -2
  26. package/scripts/advocacy-outcomes.mjs +4 -808
  27. package/scripts/capability-registry.mjs +4 -876
  28. package/scripts/corpus-qa.mjs +44 -6
  29. package/scripts/doc-currency.mjs +30 -2
  30. package/scripts/gates.mjs +4 -146
  31. package/scripts/goal-match.mjs +4 -398
  32. package/scripts/hook-registry.mjs +4 -567
  33. package/scripts/issue-watch.mjs +108 -0
  34. package/scripts/learning-enable.mjs +4 -380
  35. package/scripts/lesson-promote.mjs +4 -262
  36. package/scripts/memory-doctor.mjs +4 -345
  37. package/scripts/nightly-controller.mjs +4 -66
  38. package/scripts/nightly-wrapper.sh +23 -1
  39. package/scripts/proactivity-metrics.mjs +8 -1
  40. package/scripts/qe/ux-suite.mjs +72 -1
  41. package/scripts/release-abort-stale.mjs +111 -0
  42. package/scripts/release-convergence-watchdog.mjs +119 -0
  43. package/scripts/release-transaction-provider.mjs +61 -7
  44. package/scripts/release-transaction.mjs +63 -17
  45. package/scripts/self-update.mjs +63 -10
  46. package/scripts/user-settings.mjs +4 -640
@@ -0,0 +1,98 @@
1
+ // nightly-controller.mjs — a thin adapter around the installer's one scheduler implementation.
2
+ //
3
+ // It does not write a plist, call launchctl, or invent platform behavior. Both the installer and the
4
+ // console reach the same `bin/install.mjs --enable-nightly/--disable-nightly` door; this adapter only
5
+ // supplies structured status and captures its exit result for the console.
6
+
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ // ROOT is the tree that holds `bin/install.mjs`, and it is resolved by an EXACT layout test rather
14
+ // than by `..` — because `..` means two different things since this file moved into the payload
15
+ // (ADR-065). From `<root>/scripts/` it was the root; from `<root>/plugin/scripts/` it is
16
+ // `<root>/plugin`, and `<root>/plugin/bin/install.mjs` does not exist. Caught live by
17
+ // console-apply-timings.test.mjs, which drove a real /api/apply through the console and got
18
+ // `Error: Cannot find module '<root>/plugin/bin/install.mjs'` back inside a 200 response — a remedy
19
+ // that reported failure honestly, but failed for a packaging reason nobody would have guessed.
20
+ //
21
+ // The test is exact, not a heuristic: this file's directory IS `<candidate>/plugin/scripts` if and
22
+ // only if `<candidate>` is a non-flattened root. A flattened install (the Spine's versions/<gen>/,
23
+ // the plugin cache's <ver>/) has no `plugin/` level, so `../..` is some unrelated parent and the
24
+ // answer falls back to `..` — where `bin/` also does not exist, and applyNightlyChoice() then
25
+ // reports that honestly instead of silently spawning nothing. nightlyStatus() only reads a plist and
26
+ // needs no installer at all, so status stays correct in every layout.
27
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
28
+ const ROOT = path.resolve(HERE, '..', '..', 'plugin', 'scripts') === HERE
29
+ ? path.resolve(HERE, '..', '..')
30
+ : path.resolve(HERE, '..');
31
+ const INSTALLER = path.join(ROOT, 'bin', 'install.mjs');
32
+
33
+ /**
34
+ * THE NIGHTLY JOB'S NAME, stated once for everything that has to recognise it.
35
+ *
36
+ * Exported because issue #113 was two files disagreeing about this exact string with nothing to
37
+ * notice: bin/install.mjs writes a LaunchAgent labelled `com.ruvnet.brain-update`, while
38
+ * capability-registry.mjs looked for launchd jobs matching `/(nightly|refresh)/` — so the console
39
+ * reported "not installed" about the very job the installer had loaded, scheduled and run. A label
40
+ * is an interface between the writer and everyone who looks for it; spelling it out per reader is
41
+ * how the two drift apart silently.
42
+ *
43
+ * bin/install.mjs still holds its own literal (installer-owned code, changed under its own review);
44
+ * tests/unit/nightly-job-identity.test.mjs asserts the two are the same string, so a drift is a red
45
+ * test rather than a capability that quietly disappears from the console.
46
+ */
47
+ export const NIGHTLY_LABEL = 'com.ruvnet.brain-update';
48
+
49
+ export function nightlyArtifact({ env = process.env, platform = process.platform } = {}) {
50
+ const home = env.HOME || os.homedir();
51
+ return {
52
+ supported: platform === 'darwin',
53
+ platform,
54
+ path: path.join(home, 'Library', 'LaunchAgents', `${NIGHTLY_LABEL}.plist`),
55
+ label: NIGHTLY_LABEL,
56
+ };
57
+ }
58
+
59
+ export function nightlyStatus(options = {}) {
60
+ const artifact = nightlyArtifact(options);
61
+ if (!artifact.supported) {
62
+ return { state: 'unsupported', evidence: `No reversible scheduler adapter is implemented for ${artifact.platform}.`, artifact };
63
+ }
64
+ const present = fs.existsSync(artifact.path);
65
+ return {
66
+ state: present ? 'on' : 'off',
67
+ evidence: present ? `LaunchAgent plist exists at ${artifact.path}` : `No LaunchAgent plist at ${artifact.path}`,
68
+ artifact,
69
+ };
70
+ }
71
+
72
+ export function applyNightlyChoice(enabled, options = {}) {
73
+ if (typeof enabled !== 'boolean') return { ok: false, log: 'nightly must be true or false' };
74
+ const env = options.env || process.env;
75
+ const before = nightlyStatus({ ...options, env });
76
+ if (!before.artifact.supported) return { ok: false, state: before, log: before.evidence };
77
+ const run = spawnSync(process.execPath, [
78
+ options.installer || INSTALLER,
79
+ enabled ? '--enable-nightly' : '--disable-nightly',
80
+ ], {
81
+ env: { ...env, RUVNET_BRAIN_IMPORT_ONLY: '0' },
82
+ cwd: options.cwd || ROOT,
83
+ encoding: 'utf8',
84
+ shell: false,
85
+ timeout: options.timeout || 30_000,
86
+ });
87
+ const after = nightlyStatus({ ...options, env });
88
+ const desired = enabled ? 'on' : 'off';
89
+ const ok = !run.error && run.status === 0 && after.state === desired;
90
+ return {
91
+ ok,
92
+ before,
93
+ after,
94
+ log: ok
95
+ ? `Nightly refresh is ${desired}; verified from ${after.artifact.path}.`
96
+ : `Nightly refresh did not reach ${desired}: ${run.error?.message || run.stderr?.trim() || run.stdout?.trim() || `exit ${run.status}`}`,
97
+ };
98
+ }
@@ -66,6 +66,18 @@ function validSettings(raw = {}) {
66
66
  newProjectDefaults: source.newProjectDefaults === true,
67
67
  advocacy: Number.isInteger(source.advocacy) && source.advocacy >= 1 && source.advocacy <= 5
68
68
  ? source.advocacy : 3,
69
+ // ADR-063 / issue #103. Unknown keys are DROPPED by this function, so a setting absent here is
70
+ // silently unreadable no matter what the Console writes — which is how the first cut of the
71
+ // managed-memory boundary read `advise` even when the file said `block`.
72
+ //
73
+ // NOTE THE DUPLICATION, deliberately not "fixed" here: scripts/user-settings.mjs SETTINGS_SCHEMA
74
+ // is the authority for these keys and this is a second enumeration of it. Deriving one from the
75
+ // other is the obvious move and it is the WRONG one — this file ships inside plugin/, that one
76
+ // does not, so the import would resolve in the checkout and throw ERR_MODULE_NOT_FOUND on a real
77
+ // install. Same trap as kb/forge-update.mjs reaching for ../scripts. The honest fix is a drift
78
+ // test that reads both files, not a bridge that breaks installs.
79
+ managedMemoryBoundary: ['advise', 'read-only', 'block'].includes(source.managedMemoryBoundary)
80
+ ? source.managedMemoryBoundary : 'advise',
69
81
  };
70
82
  }
71
83
 
@@ -263,6 +275,12 @@ export function seedProjectDefaults(options = {}) {
263
275
 
264
276
  if (process.argv.includes('--learning-scope')) {
265
277
  process.stdout.write(`${loadRuntimePreferences().values.learningScope}\n`);
278
+ } else if (process.argv.includes('--managed-memory-boundary')) {
279
+ // ADR-063 / issue #103. hijack-ruvnet.sh is POSIX sh and must not parse JSON, so it asks here —
280
+ // the same idiom learn-capture.sh already uses for --learning-scope. Falls back to the shipped
281
+ // default rather than erroring: a hook that cannot read a preference must never refuse a command
282
+ // because of it, so an unreadable settings file degrades to `advise`, which blocks nothing.
283
+ process.stdout.write(`${loadRuntimePreferences().values.managedMemoryBoundary || 'advise'}\n`);
266
284
  } else if (process.argv.includes('--seed-project')) {
267
285
  const result = seedProjectDefaults();
268
286
  if (!result.ok) process.exitCode = 1;
@@ -492,14 +492,14 @@ export async function runSessionStart({
492
492
  }
493
493
  let confidenceInstruction;
494
494
  if (readiness.state === 'ready') {
495
- emit('USER-LEVEL: one brain (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project. search_ruvnet is ready and live; the grounding hooks are active.');
495
+ emit('USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is ready and live; the grounding hooks are active.');
496
496
  confidenceInstruction = `Open your FIRST response with ONE short, warm confirmation in your own words (2-3 lines, then move on; never repeat it this session). It must say "🧠 RuvNet Brain active (v${bannerVersion})" — ONE version, in parentheses, always; never a second number, never a bundle tag beside it — and convey: it grounds rUv's stack (RVF, Ruflo, AgentDB, SPARC, agentic-flow…) in his real source rather than guessing; npx github:stuinfla/ruvnet-brain --doctor checks it; ${consoleInvoke} opens a visual settings page.`;
497
497
  } else if (readiness.state === 'degraded') {
498
498
  const receipt = readiness.receipt || {};
499
- emit(`USER-LEVEL: one brain (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project. search_ruvnet is registered but degraded (${receipt.phase || 'startup'}: ${receipt.error || 'readiness failed'}); the grounding hooks remain active.`);
499
+ emit(`USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is registered but degraded (${receipt.phase || 'startup'}: ${receipt.error || 'readiness failed'}); the grounding hooks remain active.`);
500
500
  confidenceInstruction = `Open your FIRST response with ONE short line: "🧠 RuvNet Brain active (v${bannerVersion}) — search is degraded right now." Do not claim source grounding until a real search succeeds. npx github:stuinfla/ruvnet-brain --doctor shows the current verdict; ${consoleInvoke} opens the Console.`;
501
501
  } else {
502
- emit('USER-LEVEL: one brain (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project. search_ruvnet is registered; live readiness is not yet proven. The grounding hooks are active.');
502
+ emit('USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is registered; live readiness is not yet proven. The grounding hooks are active.');
503
503
  confidenceInstruction = `Open your FIRST response with ONE short line: "🧠 RuvNet Brain active (v${bannerVersion}) — search is registered and will prove readiness on first use." Do not claim source grounding until a real search returns a citation. npx github:stuinfla/ruvnet-brain --doctor shows the current verdict; ${consoleInvoke} opens the Console.`;
504
504
  }
505
505
  emit(confidenceInstruction);
@@ -77,12 +77,25 @@ import { spawnSync } from 'node:child_process';
77
77
  import { fileURLToPath, pathToFileURL } from 'node:url';
78
78
  import { readStdinBounded } from './hook-input.mjs';
79
79
 
80
- // <codeRoot>/plugin/scripts/unprompted-runtime.mjs → <codeRoot>. Resolved from THIS file's own
81
- // location so it is correct under the Stable Spine (an immutable versions/<gen> tree) and in a dev
82
- // checkout alike — hook-shim.mjs has already chosen the tree by the time it dispatches this body.
80
+ // WHERE THIS FILE'S SIBLINGS LIVE. Resolved from THIS file's own location so it is correct under the
81
+ // Stable Spine (an immutable versions/<gen> tree) and in a dev checkout alike — hook-shim.mjs has
82
+ // already chosen the tree by the time it dispatches this body.
83
+ //
84
+ // THIS USED TO ALSO COMPUTE `CODE_ROOT = SCRIPTS_DIR/../..`, and that line was WRONG in exactly the
85
+ // way anticipate.sh's was (PR #114): it assumed `scripts/` is a sibling of `plugin/`, which holds
86
+ // ONLY in a git checkout. Both shipped layouts flatten the `plugin/` level —
87
+ //
88
+ // ~/.cache/ruvnet-brain/versions/<gen>/scripts/… → ../.. = …/versions
89
+ // ~/.claude/plugins/cache/…/<ver>/scripts/… → ../.. = …/ruvnet-brain
90
+ // <src>/plugin/scripts/… → ../.. = <src> ← only this one
91
+ //
92
+ // — so `$CODE_ROOT/scripts/user-settings.mjs` pointed at nothing on every real install. Both consumers
93
+ // (the 1–5 advocacy dial and the DismissalLedger, wired at SETTINGS_MODULE/ADVOCACY_MODULE below)
94
+ // `catch` an import failure and fall back to defaults, so the dial was silently UNREAD for every user
95
+ // while the code read as if it were honoured. Same defect class, same fix: the modules are SIBLINGS of
96
+ // this file inside the payload, so name them from SCRIPTS_DIR and never reach outside it.
83
97
  const SELF = fileURLToPath(import.meta.url);
84
- const SCRIPTS_DIR = path.dirname(SELF); // <codeRoot>/plugin/scripts
85
- const CODE_ROOT = path.resolve(SCRIPTS_DIR, '..', '..'); // <codeRoot>
98
+ const SCRIPTS_DIR = path.dirname(SELF); // the payload's scripts/ dir, whatever its parent
86
99
 
87
100
  // The CC event name the shim forwarded. No event → nothing to run; stay silent.
88
101
  const EVENT = process.argv[2] || '';
@@ -235,8 +248,10 @@ if (!candidates.length) silent();
235
248
  // RUVNET_SETTINGS_FILE); suppression comes from advocacy-outcomes.shouldStillOffer()/record(). Both
236
249
  // are loaded only if an advocacy/promotion candidate is actually present, so a lesson- or alarm-only
237
250
  // event pays nothing for them.
238
- const SETTINGS_MODULE = process.env.RUVNET_USER_SETTINGS_MODULE || path.join(CODE_ROOT, 'scripts', 'user-settings.mjs');
239
- const ADVOCACY_MODULE = process.env.RUVNET_ADVOCACY_OUTCOMES_MODULE || path.join(CODE_ROOT, 'scripts', 'advocacy-outcomes.mjs');
251
+ // SIBLINGS, not `$CODE_ROOT/scripts/…` — see the SCRIPTS_DIR note at the top of this file for the
252
+ // measured layouts that made the old path resolve to nothing on every shipped install.
253
+ const SETTINGS_MODULE = process.env.RUVNET_USER_SETTINGS_MODULE || path.join(SCRIPTS_DIR, 'user-settings.mjs');
254
+ const ADVOCACY_MODULE = process.env.RUVNET_ADVOCACY_OUTCOMES_MODULE || path.join(SCRIPTS_DIR, 'advocacy-outcomes.mjs');
240
255
 
241
256
  // THE 1–5 DIAL (ADR-052). This runtime is the SINGLE enforcement chokepoint (DDD-0004): the level maps
242
257
  // to exactly what each channel is allowed to deliver, and no producer decides. LEVEL_POLICY is the one