ruvnet-brain 4.7.8 → 4.7.10

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.8-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.10-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.8",
3
+ "version": "4.7.10",
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.8",
4
+ "version": "4.7.10",
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.8",
3
+ "version": "4.7.10",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -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.
@@ -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() };
@@ -103,6 +103,22 @@ export function observeRepositories(owner, { gh = runGh } = {}) {
103
103
  return { rows: withDeltas, expected, pages };
104
104
  }
105
105
 
106
+ // The row an external source reports once its upstream is gone: exactly what was last observed, so the
107
+ // stored content classifies CURRENT, nothing is re-ingested, and the row says plainly it is no longer live.
108
+ function goneExternalRow(source) {
109
+ const gone = source.ifGone;
110
+ if (!/^[0-9a-f]{40}$/i.test(String(gone.sourceCommit || ''))) {
111
+ throw new Error(`external source ${source.repository}: ifGone.sourceCommit must be the 40-hex commit last ingested`);
112
+ }
113
+ return {
114
+ name: source.repository.split('/')[1], fullName: source.repository, storeName: source.store,
115
+ url: `https://github.com/${source.repository}`, description: gone.description ?? null, homepageUrl: null,
116
+ isFork: false, upstreamIsFork: false, isArchived: false, isDisabled: false,
117
+ updatedAt: gone.updatedAt ?? null, pushedAt: gone.pushedAt ?? null, upstreamUnavailable: true,
118
+ defaultBranchRef: { name: gone.defaultBranch || 'HEAD', target: { oid: gone.sourceCommit, committedDate: gone.committedAt ?? null } },
119
+ };
120
+ }
121
+
106
122
  export function observeExternalRepositories(sources, { gh = runGh } = {}) {
107
123
  const rows = [];
108
124
  const pages = [];
@@ -110,10 +126,23 @@ export function observeExternalRepositories(sources, { gh = runGh } = {}) {
110
126
  if (!source?.store || !/^[^/]+\/[^/]+$/.test(source?.repository || '')) {
111
127
  throw new Error('external source requires a store and owner/repository');
112
128
  }
113
- const repo = JSON.parse(gh(['api', `repos/${source.repository}`]));
114
- const commit = repo.default_branch
115
- ? JSON.parse(gh(['api', `repos/${source.repository}/commits/${repo.default_branch}`]))
116
- : null;
129
+ let repo;
130
+ let commit;
131
+ try {
132
+ repo = JSON.parse(gh(['api', `repos/${source.repository}`]));
133
+ commit = repo.default_branch
134
+ ? JSON.parse(gh(['api', `repos/${source.repository}/commits/${repo.default_branch}`]))
135
+ : null;
136
+ } catch (error) {
137
+ // An upstream that GitHub reports gone (404/410) must not stop every night for every customer.
138
+ // Only a source whose policy entry records `ifGone` (the commit it was last ingested at) may be
139
+ // carried; anything else -- 403, rate limit, a mistyped repository -- still fails the observation.
140
+ if (!source.ifGone || !/\(HTTP (404|410)\)/.test(String(error.message))) throw error;
141
+ process.stderr.write(`[source-coverage] external source ${source.repository} is gone upstream; carrying the last ingested content (${source.ifGone.sourceCommit})\n`);
142
+ rows.push(goneExternalRow(source));
143
+ pages.push({ repository: source.repository, responseDigest: digest({ gone: source.repository, ifGone: source.ifGone }), count: 1, terminal: true });
144
+ continue;
145
+ }
117
146
  const row = {
118
147
  databaseId: repo.id, name: repo.name, fullName: repo.full_name, storeName: source.store,
119
148
  url: repo.html_url, description: repo.description || null, homepageUrl: repo.homepage || null,
@@ -192,6 +221,7 @@ export function canonicalRepositoryRows(rows = []) {
192
221
  } }),
193
222
  isArchived: repo?.isArchived === true,
194
223
  isDisabled: repo?.isDisabled === true,
224
+ ...(repo?.upstreamUnavailable === true ? { upstreamUnavailable: true } : {}),
195
225
  diskUsage: repo?.diskUsage,
196
226
  updatedAt: repo?.updatedAt ?? null,
197
227
  pushedAt: repo?.pushedAt ?? null,
@@ -384,6 +414,7 @@ export function classifyRepository(repo, evidence, exclusion = null, outcome = n
384
414
  status = 'FAILED';
385
415
  reasons.push(outcome.integrity);
386
416
  }
417
+ if (repo.upstreamUnavailable === true) reasons.push(`upstream ${repo.fullName} is no longer reachable (HTTP 404/410); serving the content last ingested at ${upstreamSha}`);
387
418
  const record = {};
388
419
  if (disposed && outcome?.carry && status === 'STALE') record.carry = { ...outcome.carry };
389
420
  if (disposed && outcome?.failure && status === 'MISSING') record.failure = { ...outcome.failure };
@@ -396,6 +427,7 @@ export function classifyRepository(repo, evidence, exclusion = null, outcome = n
396
427
  capabilityCardPresent: evidence.cardPresent === true },
397
428
  disposition,
398
429
  archived: repo.isArchived === true,
430
+ ...(repo.upstreamUnavailable === true ? { upstreamUnavailable: true } : {}),
399
431
  ...(repo.forkDelta ? { forkDelta: { ...repo.forkDelta } } : {}),
400
432
  upstream: { sha: upstreamSha, committedAt: repo.defaultBranchRef?.target?.committedDate || null,
401
433
  pushedAt: repo.pushedAt, updatedAt: repo.updatedAt },