ruvnet-brain 4.7.7 → 4.7.9

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
@@ -9,7 +9,7 @@ Created: 2026-06-29 22:36:38 EDT
9
9
 
10
10
  ### Discover the stack. Read the source. Build with confidence.
11
11
 
12
- [![RuvNet Brain version 4.6.2 — README updated 2026-10-10 EDT](https://img.shields.io/badge/version_4.7.7-updated_2026--10--10_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](plugin/.claude-plugin/plugin.json)
12
+ [![RuvNet Brain version 4.6.2 — README updated 2026-10-10 EDT](https://img.shields.io/badge/version_4.7.9-updated_2026--10--10_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](plugin/.claude-plugin/plugin.json)
13
13
 
14
14
  **A source-grounded companion for Claude Code and Codex that helps you find and use Reuven Cohen's (rUv's) RuvNet ecosystem.**
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.7.7",
3
+ "version": "4.7.9",
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 a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
4
- "version": "4.7.7",
4
+ "version": "4.7.9",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.7.7",
3
+ "version": "4.7.9",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -319,6 +319,18 @@ async function consequentialRecall({ prompt, root, store, bin, scratch, env, dea
319
319
  records: selected.map(({ category, namespace, key, storePath, valueDigest }) => ({ category, namespace, key, storePath, valueDigest })), authority: false } };
320
320
  }
321
321
 
322
+ /**
323
+ * Quiet by default (2026-10-15): a recall that found nothing because it timed out or was unavailable says
324
+ * nothing — "Search timed out; no records verified" on every prompt under load is noise, not information.
325
+ * RUVNET_STOP_NUDGES=full keeps the honest status line.
326
+ */
327
+ export function recallIsWorthSurfacing(r, env = process.env) {
328
+ if (!r?.block) return false;
329
+ if (String(env.RUVNET_STOP_NUDGES || '').toLowerCase() === 'full') return true;
330
+ if (Array.isArray(r.picks) && r.picks.length) return true;
331
+ return !/\b(timed[- ]out|unavailable)\b/i.test(String(r.outcome || r.status || r.block));
332
+ }
333
+
322
334
  export async function recall({ prompt, projectDir = process.cwd(), env = process.env, deadlineMs, ruflo, scratch,
323
335
  binding = {}, signal, absoluteDeadline, consequential = false, enumerateKeys = enumerateCanonicalKeys } = {}) {
324
336
  const started = Date.now();
@@ -451,6 +463,7 @@ async function main() {
451
463
  const projectDir = typeof ev.cwd === 'string' && ev.cwd ? ev.cwd : process.cwd();
452
464
  const r = await recall({ prompt, projectDir, deadlineMs: Math.max(1, DEFAULT_DEADLINE_MS - (Date.now() - started)) });
453
465
  if (!r.block) return;
466
+ if (!recallIsWorthSurfacing(r)) return;
454
467
  const hash = crypto.createHash('sha256').update(r.block).digest('hex').slice(0, 12);
455
468
  // Hash only, never raw memory values, enters the per-session dedupe markers.
456
469
  process.stdout.write(`${hash}\n${r.block}\n`);
@@ -709,7 +709,9 @@ function securityAlertWork() {
709
709
  }
710
710
 
711
711
  const observations = [...artifactOpenWork(), ...redCiOpenWork(), ...openPrWork(), ...securityAlertWork()];
712
- if (observations.length) console.error(JSON.stringify({ kind: 'continuation-advisory',
712
+ // Quiet by default (2026-10-15): an observed open issue/PR/CI row is information the user can read on GitHub;
713
+ // printing it on every Stop turned an advisory into per-turn noise. RUVNET_STOP_NUDGES=full restores it.
714
+ if (observations.length && STOP_NUDGES_FULL) console.error(JSON.stringify({ kind: 'continuation-advisory',
713
715
  authority: false, items: observations.map(({ text, at }) => ({ text, at })) }));
714
716
  // ONE correction per turn: a single item however many claims the answer made; the stop_hook_active
715
717
  // exit and the cooldown lock below bound it to one request per stop episode.
@@ -31,6 +31,8 @@ import { append as appendOutcome, actionKey, recordRefusal, resolve as resolveOu
31
31
  // see a host-native spelling — the 4.4 measurement was a Grok write silently allowed. hook-input.mjs.
32
32
  import { normalizePayloadText } from './hook-input.mjs';
33
33
 
34
+ const QUIET_FULL = String(process.env.RUVNET_STOP_NUDGES || '').toLowerCase() === 'full';
35
+
34
36
  const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
35
37
  const EVENT = process.argv[2] || '';
36
38
 
@@ -415,7 +417,10 @@ function reportBudget({ session, unconsulted, trace, started, budgetMs, refused
415
417
  try {
416
418
  appendOutcome({ kind: 'budget-exceeded', event: EVENT, session, unconsulted, elapsedMs: elapsed, budgetMs, ts: Date.now() });
417
419
  } catch { /* a ledger must never break a tool call */ }
418
- process.stderr.write(
420
+ // Quiet by default (2026-10-15): the outcome is already in the ledger above. On an ALLOW, a stderr line only
421
+ // makes the host show a hook "error" for a decision that was made. It is kept when the action was refused
422
+ // and under RUVNET_STOP_NUDGES=full.
423
+ if (refused || QUIET_FULL) process.stderr.write(
419
424
  `[decision-gate] ${budgetMs}ms budget exhausted after ${elapsed}ms — ${refused ? 'REFUSED; UNCONSULTED' : 'ALLOWED WITHOUT CONSULTING'}: `
420
425
  + `${unconsulted.join(', ')}. These policies did not vote; ${refused ? 'another policy refused the action.' : 'this allow is a timeout, not a verdict.'}\n`,
421
426
  );
@@ -424,7 +429,7 @@ function reportBudget({ session, unconsulted, trace, started, budgetMs, refused
424
429
  function reportSelfSkipped({ session, selfSkipped }) {
425
430
  for (const r of selfSkipped) {
426
431
  try { appendOutcome({ kind: 'policy-skipped', event: EVENT, session, policy: r.id, reason: r.reason, ts: Date.now() }); } catch { /* a ledger must never break a tool call */ }
427
- process.stderr.write(`[decision-gate] ${r.reason || `${r.id} skipped`} — ${r.id} did not vote; it supplied no authorization verdict.\n`);
432
+ if (QUIET_FULL) process.stderr.write(`[decision-gate] ${r.reason || `${r.id} skipped`} — ${r.id} did not vote; it supplied no authorization verdict.\n`);
428
433
  }
429
434
  }
430
435
  /**
@@ -16,7 +16,20 @@ export function sharedLockStatus({ brainHome, alive = pid => { try { process.kil
16
16
  export function acquireDeveloperLock({ brainHome, token = process.env.RUVNET_DEVELOPER_UPDATE_TOKEN, pid = process.pid, alive } = {}) {
17
17
  const status = sharedLockStatus({ brainHome, alive });
18
18
  if (token && status.state === 'running' && status.owner.token === token) return { token, ownerPid: status.owner.pid, inherited: true, release() {} };
19
- if (status.state !== 'idle') throw Error(`developer update lock ${status.state}; ${status.directory}`);
19
+ // A dead owner's lock (dead for 10+ minutes) must not wedge every future update (measured 2026-10-15: a coordinator killed mid-run
20
+ // left `stale` behind and the automatic updater refused with "developer update lock stale" until a person
21
+ // removed the directory). Reclaim it: move the directory aside atomically (so two reclaimers cannot both
22
+ // win), then fall through to the normal atomic mkdir claim. Only a PROVABLY dead pid qualifies; a live,
23
+ // unknown or malformed lock still refuses.
24
+ const STALE_RECLAIM_MS = 10 * 60 * 1000;
25
+ const ageMs = status.state === 'stale' ? Date.now() - Date.parse(status.owner?.startedAt || '') : NaN;
26
+ if (status.state === 'stale' && Number.isFinite(ageMs) && ageMs >= STALE_RECLAIM_MS) {
27
+ const tomb = `${status.directory}.stale-${process.pid}-${Date.now()}`;
28
+ try { fs.renameSync(status.directory, tomb); fs.rmSync(tomb, { recursive: true, force: true }); }
29
+ catch { /* another reclaimer won, or the owner came back: re-read below */ }
30
+ const after = sharedLockStatus({ brainHome, alive });
31
+ if (after.state !== 'idle') throw Error(`developer update lock ${after.state}; ${after.directory}`);
32
+ } else if (status.state !== 'idle') throw Error(`developer update lock ${status.state}; ${status.directory}`);
20
33
  fs.mkdirSync(path.dirname(status.directory), { recursive: true });
21
34
  fs.mkdirSync(status.directory, { mode: 0o700 }); // atomic claim; racing owners get EEXIST
22
35
  const owner = { pid, token: crypto.randomUUID(), startedAt: new Date().toISOString() };
@@ -209,6 +209,15 @@ if (!entry) {
209
209
  // forgets to declare one fails toward the pre-ADR-054 behaviour rather than toward silent death.
210
210
  if (BRAIN_OFF && entry.offBehavior === 'silence') process.exit(0);
211
211
 
212
+ // QUIET BY DEFAULT (owner, 2026-10-15). These advisory hooks only ever add text to a turn (advocacy offers,
213
+ // "ADRs are living plans", MetaHarness/QE offers, parallel-work sizing, the answered-without-searching
214
+ // nudge). None of them blocks or protects anything, and together they were the constant noise on every
215
+ // prompt. They stay registered and run only when RUVNET_STOP_NUDGES=full, so nothing is deleted and the
216
+ // tests still exercise them. The blocking hooks (decision-gate), the grounding directive and the silent
217
+ // capture hooks are unaffected.
218
+ const QUIET_BY_DEFAULT = new Set(['unprompted-speech', 'capacity-aware-parallel-work', 'grounding-turn-gate']);
219
+ if (QUIET_BY_DEFAULT.has(hookId) && String(process.env.RUVNET_STOP_NUDGES || '').toLowerCase() !== 'full') process.exit(0);
220
+
212
221
  // Read a bounded payload before dispatch. The ground body owns the quiet-prompt
213
222
  // decision because it resolves canonical memory beyond the hook process's cwd.
214
223
  let hookInput = null;