ruvnet-brain 4.5.17 → 4.6.0

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 (36) hide show
  1. package/README.md +15 -19
  2. package/bin/developer-update.mjs +3 -0
  3. package/bin/install.mjs +14 -2
  4. package/bin/nightly-refresh.mjs +17 -1
  5. package/console/CONTRACT.md +47 -3
  6. package/console/app.js +112 -20
  7. package/package.json +4 -2
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/commands/brain-console.md +13 -1
  11. package/plugin/commands/configure.md +2 -2
  12. package/plugin/commands/rnb.md +13 -1
  13. package/plugin/commands/rnbc.md +13 -1
  14. package/plugin/commands/rvbc.md +13 -1
  15. package/plugin/commands/rvcb.md +13 -1
  16. package/plugin/scripts/automatic-update.mjs +11 -2
  17. package/plugin/scripts/continuation-gate.mjs +2 -1
  18. package/plugin/scripts/decision-gate.mjs +0 -16
  19. package/plugin/scripts/developer-update-cleanup.mjs +38 -0
  20. package/plugin/scripts/developer-update-lock.mjs +30 -0
  21. package/plugin/scripts/developer-update-maintenance.mjs +178 -0
  22. package/plugin/scripts/developer-update-owner.mjs +24 -0
  23. package/plugin/scripts/developer-update-policy.mjs +48 -0
  24. package/plugin/scripts/developer-update.mjs +435 -0
  25. package/plugin/scripts/host-update.mjs +6 -3
  26. package/plugin/scripts/nightly-scheduler.mjs +55 -16
  27. package/plugin/scripts/plugin-artifact-proof.mjs +135 -0
  28. package/plugin/skills/ruvnet-brain/SKILL.md +2 -2
  29. package/scripts/codex-managed-terminal.mjs +1 -1
  30. package/scripts/console-runtime-identity.mjs +2 -0
  31. package/scripts/console-suite-update.mjs +117 -0
  32. package/scripts/hook-qualify-core.mjs +29 -0
  33. package/scripts/onboarding-console.mjs +41 -12
  34. package/scripts/release-qualification-contract.mjs +14 -0
  35. package/scripts/remedy-registry.mjs +1 -0
  36. package/scripts/stack-sync.mjs +19 -132
@@ -36,6 +36,9 @@ import { spawnSync } from 'node:child_process';
36
36
  import fs from 'node:fs';
37
37
  import os from 'node:os';
38
38
  import path from 'node:path';
39
+ import { runDeveloperUpdate, readDeveloperUpdateConfig } from '../plugin/scripts/developer-update.mjs';
40
+ import { FAMILY, PLUGIN_MARKETPLACES, cmpVersion, pickTargetTag, normalizeNpmDistTags } from '../plugin/scripts/developer-update-policy.mjs';
41
+ export { FAMILY, PLUGIN_MARKETPLACES, cmpVersion, pickTargetTag, normalizeNpmDistTags } from '../plugin/scripts/developer-update-policy.mjs';
39
42
 
40
43
  const HOME = os.homedir();
41
44
 
@@ -78,9 +81,9 @@ const RECEIPT = path.join(HOME, '.cache/ruvnet-brain/stack-sync-receipt.json');
78
81
  const PLUGINS_DIR = path.join(HOME, '.claude', 'plugins');
79
82
 
80
83
  // The tag policy. ONE table — the single source of truth for "what SHOULD be installed".
81
- // The orchestration core tracks alpha (rUv ships fast: 3.26 -> 3.28 inside one 18-hour window).
82
- // Everything else tracks latest. Unlisted packages get DEFAULT_TAG.
83
- export const TAG_POLICY = { ruflo: 'alpha', '@claude-flow/cli': 'alpha' };
84
+ // The recommended default is latest. The canonical owner config can explicitly select alpha.
85
+ // Unlisted packages still get DEFAULT_TAG; installed Kit retains next/latest ordering.
86
+ export const TAG_POLICY = { ruflo: 'latest', '@claude-flow/cli': 'latest' };
84
87
  export const DEFAULT_TAG = 'latest';
85
88
 
86
89
  // Resolve a package's TARGET version to the NEWEST of its candidate tags — the policy tag AND latest —
@@ -91,19 +94,10 @@ export const DEFAULT_TAG = 'latest';
91
94
  // 3.32.0 @alpha target, so it correctly refused to downgrade and never chased 3.32.7). Considering both
92
95
  // tags and taking the higher version fixes that in EITHER direction: alpha-leads → track alpha;
93
96
  // latest-leads → track latest. You are never left behind the newest thing rUv actually published.
94
- export function pickTargetTag(tags, want, defaultTag = DEFAULT_TAG) {
95
- if (!tags) return { tag: null, target: null };
96
- const candidates = [...new Set([want, defaultTag])].filter((t) => tags[t]);
97
- let tag = null, target = null;
98
- for (const t of candidates) {
99
- if (target === null || cmpVersion(tags[t], target) > 0) { tag = t; target = tags[t]; }
100
- }
101
- return { tag, target };
102
- }
103
97
 
104
98
  // What counts as "the stack": an explicit allow-list pattern, not a loose scope match, so a stray
105
99
  // package can never be swept into a global install by accident.
106
- export const FAMILY = /^(ruflo|ruvector|ruvector-extensions|ruvi|ruvbot|qudag|flow-nexus|agent-browser|agent-browser-mcp|agentic-flow|agentic-qe|agentic-robotics|agentic-payments|ruv-swarm|@ruvector\/|@claude-flow\/|@metaharness\/|@agentic-robotics\/)/;
100
+
107
101
 
108
102
  // The plugin side of the same allow-list intent (ISSUE #22). Plugin identifiers are marketplace
109
103
  // names (`ruflo-core`, `ruvnet-brain`, `cog-beehive-monitor`), NOT npm package names, so FAMILY
@@ -114,7 +108,7 @@ export const FAMILY = /^(ruflo|ruvector|ruvector-extensions|ruvi|ruvbot|qudag|fl
114
108
  // stray third-party plugin can never be swept into the RuvNet stack by accident. FAMILY is still
115
109
  // applied as a secondary matcher, so a future rUv plugin shipped through a different marketplace is
116
110
  // still counted.
117
- export const PLUGIN_MARKETPLACES = new Set(['ruflo', 'ruview', 'ruvnet-brain', 'cognitum']);
111
+
118
112
 
119
113
  const log = (m) => console.log(m);
120
114
  const die = (m) => { console.error(`\n FAILED: ${m}`); process.exit(1); };
@@ -123,30 +117,7 @@ const die = (m) => { console.error(`\n FAILED: ${m}`); process.exit(1); };
123
117
  // Prerelease-aware per semver: a prerelease sorts BEFORE its release (3.28.0-alpha.1 < 3.28.0),
124
118
  // and numeric identifiers compare numerically — alpha.9 < alpha.10, which a string compare gets
125
119
  // backwards, and a string compare is precisely the bug this whole file exists to kill.
126
- export function cmpVersion(a, b) {
127
- const split = (v) => {
128
- const [core, pre] = String(v).split('-');
129
- return [core.split('.').map((n) => parseInt(n, 10) || 0), pre ? pre.split('.') : null];
130
- };
131
- const [ac, ap] = split(a);
132
- const [bc, bp] = split(b);
133
- for (let i = 0; i < 3; i++) {
134
- const d = (ac[i] || 0) - (bc[i] || 0);
135
- if (d !== 0) return d < 0 ? -1 : 1;
136
- }
137
- if (!ap && !bp) return 0;
138
- if (ap && !bp) return -1;
139
- if (!ap && bp) return 1;
140
- for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
141
- const x = ap[i], y = bp[i];
142
- if (x === undefined) return -1;
143
- if (y === undefined) return 1;
144
- const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
145
- if (nx && ny) { const d = parseInt(x, 10) - parseInt(y, 10); if (d) return d < 0 ? -1 : 1; }
146
- else if (x !== y) return x < y ? -1 : 1;
147
- }
148
- return 0;
149
- }
120
+
150
121
  export const isBehind = (installed, target) => cmpVersion(installed, target) < 0;
151
122
 
152
123
  export function installedVersion(pkg, lib = GLOBAL_LIB) {
@@ -219,7 +190,7 @@ function listInstalled({ lib = GLOBAL_LIB, pluginsDir = PLUGINS_DIR } = {}) {
219
190
  function registryTags(pkg) {
220
191
  const r = spawnSync('npm', ['view', pkg, 'dist-tags', '--json'], { encoding: 'utf8', timeout: 30000 });
221
192
  if (r.status !== 0 || !r.stdout) return null;
222
- try { return JSON.parse(r.stdout); } catch { return null; }
193
+ try { return normalizeNpmDistTags(JSON.parse(r.stdout)); } catch { return null; }
223
194
  }
224
195
 
225
196
  // A second copy of a stack package in the npx cache can only ever SHADOW the global one.
@@ -251,13 +222,13 @@ export function findShadows(npxCache = NPX_CACHE, lib = GLOBAL_LIB) {
251
222
  // isBehind — this function assigns a state label, it does not compare versions itself except through
252
223
  // the single comparator. Extracted so audit() (CLI, may exit) and auditModel() (embedders, never
253
224
  // exits) share ONE classification, never two that can drift.
254
- export function classify(pkgs) {
225
+ export function classify(pkgs, { channel = readDeveloperUpdateConfig().channel } = {}) {
255
226
  return pkgs.map((p) => {
256
227
  // Installed records prove presence, not marketplace currency.
257
228
  if (p.source === 'plugin') {
258
229
  return { ...p, tag: 'plugin', target: null, state: p.installed ? 'INSTALLED_UNVERIFIED' : 'BROKEN', evidence: 'local install record only; marketplace revision not checked' };
259
230
  }
260
- const want = TAG_POLICY[p.name] || DEFAULT_TAG;
231
+ const want = p.name === '@pacphi/agentic-kit' ? 'next' : channel === 'alpha' && FAMILY.test(p.name) ? 'alpha' : DEFAULT_TAG;
261
232
  const tags = registryTags(p.name);
262
233
  // Newest of {policy tag, latest} — see pickTargetTag: "track @alpha" alone pinned the core behind
263
234
  // @latest on 2026-07-18. A package with no alpha tag falls through to latest.
@@ -348,95 +319,11 @@ function report({ rows, shadows, stale }) {
348
319
  return { behind: rows.filter((r) => r.state === 'BEHIND'), broken: rows.filter((r) => r.state === 'BROKEN'), stale };
349
320
  }
350
321
 
351
- function writeReceipt(a, installed, purged) {
352
- fs.mkdirSync(path.dirname(RECEIPT), { recursive: true });
353
- fs.writeFileSync(RECEIPT, JSON.stringify({
354
- at: new Date().toISOString(), installed, purged,
355
- packages: a.rows.map((r) => ({ name: r.name, version: r.installed, state: r.state })),
356
- }, null, 2));
357
- }
358
-
359
- // EXCLUSIVE LOCK. (Adversarial review, 2026-07-14.) Nothing stopped the nightly job and a manual
360
- // run (or two Claude Code windows) from both running `npm install -g` on the same package at the
361
- // same instant. Two concurrent installs interleaving in one node_modules dir is how you get a
362
- // half-written package — which is almost certainly how @ruvector/edge-net ended up present-but-
363
- // versionless with an orphaned `sharp` inside it. O_EXCL is atomic; a stale lock from a crashed
364
- // run is reclaimed after 20 minutes (longer than the npm timeout).
365
- const LOCK = path.join(HOME, '.cache/ruvnet-brain/stack-sync.lock');
366
- function acquireLock() {
367
- fs.mkdirSync(path.dirname(LOCK), { recursive: true });
368
- try {
369
- fs.writeFileSync(LOCK, JSON.stringify({ pid: process.pid, at: Date.now() }), { flag: 'wx' });
370
- } catch (e) {
371
- if (e.code !== 'EEXIST') throw e;
372
- let held = {};
373
- try { held = JSON.parse(fs.readFileSync(LOCK, 'utf8')); } catch { /* unparseable = stale */ }
374
- const ageMin = (Date.now() - (held.at ?? 0)) / 60000;
375
- if (ageMin < 20) {
376
- die(`another stack-sync is running (pid ${held.pid}, started ${ageMin.toFixed(1)}m ago).\n` +
377
- ` Refusing to run two installers at once — that is how packages get half-written.`);
378
- }
379
- fs.writeFileSync(LOCK, JSON.stringify({ pid: process.pid, at: Date.now() }));
380
- }
381
- const release = () => { try { fs.unlinkSync(LOCK); } catch { /* already gone */ } };
382
- process.on('exit', release);
383
- process.on('SIGINT', () => { release(); process.exit(130); });
384
- process.on('SIGTERM', () => { release(); process.exit(143); });
385
- }
386
-
387
- function sync({ dryRun = false } = {}) {
388
- if (!dryRun) acquireLock();
389
- const a = audit();
390
- const { behind, broken, stale } = report(a);
391
- const toInstall = [...behind, ...broken.filter((r) => r.target)].map((r) => `${r.name}@${r.target}`);
392
-
393
- if (!toInstall.length && !stale.length) {
394
- log(` No npm repair selected. Audit: ${JSON.stringify(summarizeAudit(a))}`);
395
- if (!dryRun) writeReceipt(a, [], []);
396
- process.exitCode = summarizeAudit(a).exitCode;
397
- return;
398
- }
399
- if (dryRun) {
400
- if (toInstall.length) log(` would install: ${toInstall.join(' ')}`);
401
- if (stale.length) log(` would purge ${stale.length} stale npx shadow(s)`);
402
- return;
403
- }
404
-
405
- // INSTALL FIRST, PURGE SECOND. (Adversarial review, 2026-07-14.) The first version purged the
406
- // shadows before installing — so a failed install left the user with their shadows gone AND the
407
- // global not yet fixed: strictly WORSE than when they started, with no way back. A repair step
408
- // that can leave you worse off than not running it is not a repair.
409
- if (toInstall.length) {
410
- log(` installing: ${toInstall.join(' ')}`);
411
- const r = spawnSync('npm', ['install', '-g', '--prefix', PREFIX, ...toInstall],
412
- { stdio: 'inherit', timeout: 15 * 60 * 1000 });
413
- if (r.status !== 0) die(`npm install -g exited ${r.status}; the stack is NOT synced. Shadows left untouched.`);
414
- }
415
-
416
- const purged = [];
417
- for (const s of stale) {
418
- // Purge the whole npx dir: it is a disposable resolution cache (npm re-creates it on demand),
419
- // and a mixed-version dir is exactly how a stale transitive copy survives a targeted delete.
420
- try { fs.rmSync(s.dir, { recursive: true, force: true }); purged.push(`${s.name}@${s.version}`); } catch { /* best effort */ }
421
- }
422
- if (purged.length) log(` purged ${purged.length} stale shadow(s): ${purged.join(', ')}`);
423
-
424
- // VERIFY AGAINST THE DISK. An installer that trusts its own exit code is a hope, not a guarantee:
425
- // the old nightly printed "auto-update finished cleanly" from npm's status alone, which is exactly
426
- // how a stack rots while every log line insists it is healthy.
427
- const wrong = [];
428
- for (const r of [...behind, ...broken]) {
429
- if (!r.target) continue;
430
- const now = installedVersion(r.name);
431
- if (now !== r.target) wrong.push(`${r.name}: expected ${r.target}, disk says ${now ?? 'MISSING'}`);
432
- }
433
- if (wrong.length) die(`npm reported success but THE DISK DISAGREES:\n - ${wrong.join('\n - ')}`);
434
-
435
- const after = audit();
436
- writeReceipt(after, toInstall, purged);
437
- process.exitCode = summarizeAudit(after).exitCode;
438
- log(` Remaining audit: ${JSON.stringify(summarizeAudit(after))}`);
439
- log(`\n synced ${toInstall.length} package(s); purged ${purged.length} shadow(s); verified against disk.`);
322
+ async function sync({ dryRun = false } = {}) {
323
+ const config = readDeveloperUpdateConfig();
324
+ const receipt = await runDeveloperUpdate({ mode: dryRun ? 'check' : 'apply', config });
325
+ log(`Coordinated update ${receipt.state}; ${receipt.steps.length} installed package owners checked.`);
326
+ if (receipt.coverage.notes.length) log(`Excluded or preserved: ${receipt.coverage.notes.join('; ')}`);
440
327
  }
441
328
 
442
329
  // Importable as a module by the tests; only acts when run as a CLI.
@@ -451,13 +338,13 @@ if (process.argv[1] && path.resolve(process.argv[1]).endsWith('stack-sync.mjs'))
451
338
  if (summary.shadows) log(' Cached copies are present; inspect invocation paths before removing any cache. No copies removed by audit.');
452
339
  process.exitCode = summary.exitCode;
453
340
  } else if (args.includes('--sync')) {
454
- sync({ dryRun: args.includes('--dry-run') });
341
+ sync({ dryRun: args.includes('--dry-run') }).catch(error => { console.error(error.message); process.exitCode = 1; });
455
342
  } else {
456
343
  log(`
457
344
  stack-sync — one global copy of the RuvNet stack. Correct. Provable.
458
345
 
459
346
  --audit report drift; exit 1 if anything is behind, broken, or shadowed
460
- --sync install what is BEHIND (never downgrades), purge npx shadows, verify against disk
347
+ --sync update existing owners through the shared coordinator; cleanup is explicit
461
348
  --dry-run with --sync: say what it would do, change nothing
462
349
 
463
350
  Tag policy: ${JSON.stringify(TAG_POLICY)}; everything else @${DEFAULT_TAG}