ruvnet-brain 4.5.16 → 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 (86) hide show
  1. package/README.md +15 -19
  2. package/bin/developer-update.mjs +3 -0
  3. package/bin/install.mjs +15 -3
  4. package/bin/nightly-refresh.mjs +17 -1
  5. package/config/practical-rule-catalog.json +111 -0
  6. package/console/CONTRACT.md +47 -3
  7. package/console/app.js +112 -20
  8. package/package.json +5 -2
  9. package/plugin/.claude-plugin/plugin.json +1 -1
  10. package/plugin/.codex-plugin/plugin.json +1 -1
  11. package/plugin/commands/brain-console.md +13 -1
  12. package/plugin/commands/configure.md +2 -2
  13. package/plugin/commands/rnb.md +13 -1
  14. package/plugin/commands/rnbc.md +13 -1
  15. package/plugin/commands/rvbc.md +13 -1
  16. package/plugin/commands/rvcb.md +13 -1
  17. package/plugin/hooks/codex-hooks.json +13 -1
  18. package/plugin/hooks/hook-contracts.json +20 -4
  19. package/plugin/scripts/adr-currency-gate.mjs +75 -31
  20. package/plugin/scripts/advocacy-outcomes.mjs +18 -4
  21. package/plugin/scripts/agentdb-recall.mjs +161 -26
  22. package/plugin/scripts/automatic-update.mjs +11 -2
  23. package/plugin/scripts/capability-inventory-receipt.mjs +16 -7
  24. package/plugin/scripts/capability-registry.mjs +55 -114
  25. package/plugin/scripts/capacity-aware-parallel-work.mjs +1 -1
  26. package/plugin/scripts/codex-hook-adapter.mjs +33 -6
  27. package/plugin/scripts/codex-hook-wrapper.mjs +23 -3
  28. package/plugin/scripts/continuation-gate.mjs +2 -1
  29. package/plugin/scripts/continuity-brief.mjs +55 -22
  30. package/plugin/scripts/continuity-events.mjs +93 -17
  31. package/plugin/scripts/continuity-hook-policy.mjs +1 -1
  32. package/plugin/scripts/continuity-journal.mjs +77 -29
  33. package/plugin/scripts/decision-gate.mjs +62 -37
  34. package/plugin/scripts/decision-outcomes.mjs +192 -199
  35. package/plugin/scripts/detach.mjs +53 -13
  36. package/plugin/scripts/developer-update-cleanup.mjs +38 -0
  37. package/plugin/scripts/developer-update-lock.mjs +30 -0
  38. package/plugin/scripts/developer-update-maintenance.mjs +178 -0
  39. package/plugin/scripts/developer-update-owner.mjs +24 -0
  40. package/plugin/scripts/developer-update-policy.mjs +48 -0
  41. package/plugin/scripts/developer-update.mjs +435 -0
  42. package/plugin/scripts/doc-currency.mjs +993 -0
  43. package/plugin/scripts/ground-ruvnet.sh +16 -3
  44. package/plugin/scripts/grounding-answer.mjs +18 -16
  45. package/plugin/scripts/grounding-stamp.sh +3 -0
  46. package/plugin/scripts/grounding-turn-evidence.mjs +60 -65
  47. package/plugin/scripts/grounding-turn-gate.mjs +34 -29
  48. package/plugin/scripts/grounding-turn-mark.mjs +101 -17
  49. package/plugin/scripts/hook-context-budget.mjs +90 -0
  50. package/plugin/scripts/hook-registry.mjs +129 -1
  51. package/plugin/scripts/hook-shim.mjs +20 -2
  52. package/plugin/scripts/host-update.mjs +6 -3
  53. package/plugin/scripts/lesson-gate.mjs +1 -1
  54. package/plugin/scripts/nightly-scheduler.mjs +55 -16
  55. package/plugin/scripts/plugin-artifact-proof.mjs +135 -0
  56. package/plugin/scripts/project-capture-queue.mjs +22 -15
  57. package/plugin/scripts/project-progression-producer.mjs +15 -8
  58. package/plugin/scripts/project-progression-reader.mjs +14 -4
  59. package/plugin/scripts/project-progression-session-start.mjs +29 -17
  60. package/plugin/scripts/project-progression-sources.mjs +24 -10
  61. package/plugin/scripts/project-progression-store.mjs +7 -2
  62. package/plugin/scripts/project-store-resolver.mjs +10 -7
  63. package/plugin/scripts/project-transition-hook.mjs +20 -12
  64. package/plugin/scripts/session-snapshot-hook.mjs +20 -9
  65. package/plugin/scripts/session-start-budget.mjs +18 -0
  66. package/plugin/scripts/session-start-core.mjs +32 -6
  67. package/plugin/scripts/session-start-trace.mjs +2 -1
  68. package/plugin/scripts/turn-outcome-capture.mjs +7 -5
  69. package/plugin/scripts/unprompted-runtime.mjs +36 -10
  70. package/plugin/scripts/user-settings.mjs +7 -1
  71. package/plugin/skills/ruvnet-brain/SKILL.md +2 -2
  72. package/scripts/codex-managed-terminal.mjs +1 -1
  73. package/scripts/console-runtime-identity.mjs +2 -0
  74. package/scripts/console-suite-update.mjs +117 -0
  75. package/scripts/doc-currency.mjs +6 -990
  76. package/scripts/hook-qualify-core.mjs +30 -1
  77. package/scripts/model-managed-prompt.mjs +1 -1
  78. package/scripts/model-managed-workflow-service.mjs +113 -14
  79. package/scripts/model-routing-controller.mjs +112 -21
  80. package/scripts/onboarding-console.mjs +41 -12
  81. package/scripts/practical-rule-selector.mjs +51 -0
  82. package/scripts/product-integrity-contract.mjs +11 -4
  83. package/scripts/release-qualification-contract.mjs +14 -0
  84. package/scripts/remedy-registry.mjs +1 -0
  85. package/scripts/source-scope-receipt.mjs +9 -4
  86. package/scripts/stack-sync.mjs +19 -132
@@ -0,0 +1,178 @@
1
+ // Explicitly enabled existing-owner stages. No cleanup, fresh installs or local-source conversion.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { cmpVersion as compare } from './developer-update-policy.mjs';
6
+ const HOME = os.homedir();
7
+ const read = p => JSON.parse(fs.readFileSync(p, 'utf8'));
8
+ function required(p) { fs.accessSync(p, fs.constants.X_OK); return p; }
9
+ export function cargoInventory(root = path.join(HOME, '.cargo')) {
10
+ return Object.entries(read(path.join(root, '.crates2.json')).installs || {}).map(([key, value]) => {
11
+ const m = /^(\S+) (\S+) \(([^)]+)\)$/.exec(key);
12
+ if (!m) throw Error(`unattributable cargo install: ${key}`);
13
+ return { name: m[1], version: m[2], source: m[3], ...value };
14
+ });
15
+ }
16
+ export function uvInventory(root = path.join(HOME, '.local/share/uv/tools')) {
17
+ if (!fs.existsSync(root)) return [];
18
+ return fs.readdirSync(root).filter(name => fs.existsSync(path.join(root, name, 'uv-receipt.toml'))).map(name => {
19
+ const receipt = fs.readFileSync(path.join(root, name, 'uv-receipt.toml'), 'utf8');
20
+ // Only plain unpinned registry requirements qualify; wheels, paths, git and pins stay unchanged.
21
+ const registry = new RegExp(`requirements\\s*=\\s*\\[\\{\\s*name\\s*=\\s*"${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\s*\\}\\]`).test(receipt);
22
+ return { name, root: path.join(root, name), registry, receipt };
23
+ });
24
+ }
25
+ export function nativeOwnerPreserved(file, beforeTarget, afterTarget, home = HOME) {
26
+ if (afterTarget.split(path.sep).includes('node_modules')) return false;
27
+ if (beforeTarget === afterTarget) return true;
28
+ const standalone = path.join(home, '.codex/packages/standalone') + path.sep;
29
+ return file === path.join(home, '.local/bin/codex') && beforeTarget.startsWith(standalone) && afterTarget.startsWith(standalone);
30
+ }
31
+ export function verifyMaintenanceStage(commands, postconditions) {
32
+ const failures = Object.entries(postconditions).filter(([, passed]) => passed !== true).map(([name]) => name);
33
+ const commandFailures = commands.filter(command => command.exitCode !== 0 || command.error);
34
+ return { ok: failures.length === 0 && commandFailures.length === 0, postconditions, commandFailures, failures };
35
+ }
36
+ function commandOutput(run, command, args, options, stage) {
37
+ const result = run.receipt ? run.receipt(command, args, options) : run(command, args, options);
38
+ const exitCode = result?.exitCode ?? result?.status ?? result?.code;
39
+ const captured = { command, args, exitCode, error: result?.error || null, stdout: result?.stdout, stderr: result?.stderr || '' };
40
+ stage?.commands.push(captured);
41
+ if (!Number.isInteger(exitCode) || exitCode !== 0 || captured.error || typeof captured.stdout !== 'string') throw Error(`provider command failed or exit evidence absent: ${command} ${args[0]} (exit ${exitCode})`);
42
+ return captured.stdout;
43
+ }
44
+ function settleStage(stage, after, postconditions, fields = {}) {
45
+ const verification = verifyMaintenanceStage(stage.commands, postconditions);
46
+ Object.assign(stage, { after, verification, ...fields, state: verification.ok ? 'completed' : 'failed' });
47
+ if (!verification.ok) throw Error(`provider postconditions failed: ${stage.owner}: ${verification.failures.join(', ')}`);
48
+ }
49
+ export async function maintenance(config, run, dryRun, { home = HOME, brainHome, locate = () => null, node = process.execPath, progress = () => {} } = {}) {
50
+ const result = { stages: [], exclusions: [] };
51
+ progress(result);
52
+ for (const [name, enabled] of Object.entries(config)) {
53
+ if (!['schemaVersion', 'scope', 'channel', 'cleanup', 'preservePackages', 'homebrew', 'uv', 'cargo', 'native', 'managedCallback'].includes(name)) throw Error(`unknown maintenance flag: ${name}`);
54
+ if (['homebrew', 'uv', 'cargo', 'native'].includes(name) && typeof enabled !== 'boolean') throw Error(`maintenance flag must be boolean: ${name}`);
55
+ }
56
+ const uvTools = config.uv ? uvInventory(path.join(home, '.local/share/uv/tools')) : [];
57
+ const hasEntries = directory => { try { return fs.readdirSync(directory).length > 0; } catch { return false; } };
58
+ if (config.homebrew && !locate('brew') && [path.join(home, '.linuxbrew/Cellar'), '/opt/homebrew/Cellar', '/usr/local/Cellar', '/home/linuxbrew/.linuxbrew/Cellar'].some(hasEntries)) throw Error('installed Homebrew formulas have no existing Homebrew manager');
59
+ const cratesFile = path.join(home, '.cargo/.crates2.json');
60
+ if (uvTools.length && !locate('uv')) throw Error('installed uv tools have no existing uv manager');
61
+ if (config.cargo && fs.existsSync(cratesFile) && cargoInventory(path.dirname(cratesFile)).length && !locate('cargo')) throw Error('installed Cargo tools have no existing Cargo manager');
62
+ if (config.native) {
63
+ for (const [file, args] of [[path.join(home, '.bun/bin/bun'), ['upgrade']], [path.join(home, '.local/bin/uv'), ['self', 'update']],
64
+ [path.join(home, '.cargo/bin/rustup'), ['self', 'update']], [path.join(home, '.local/bin/codex'), ['update']]]) {
65
+ if (!fs.existsSync(file)) { result.exclusions.push(`native owner absent: ${file}`); continue; }
66
+ const resolved = fs.realpathSync(file);
67
+ if (resolved.split(path.sep).includes('node_modules')) { result.exclusions.push(`npm owns native alias: ${file}`); continue; }
68
+ required(file);
69
+ const owner = fs.realpathSync(file), stage = { owner: file, originalTarget: owner, state: 'running', commands: [] }; result.stages.push(stage);
70
+ const before = commandOutput(run, file, ['--version'], { timeout: 30_000 }, stage); stage.before = before;
71
+ if (!dryRun) commandOutput(run, file, args, { timeout: 900_000 }, stage);
72
+ const after = commandOutput(run, file, ['--version'], { timeout: 30_000 }, stage);
73
+ const version = text => text.match(/\b(\d+\.\d+\.\d+(?:-[\w.-]+)?)/)?.[1];
74
+ const versionVerified = !!version(before) && !!version(after) && compare(version(after), version(before)) >= 0;
75
+ // Native launchers may re-point to a newer version, but stay in the same established root.
76
+ const afterOwner = fs.realpathSync(file);
77
+ const ownerPreserved = nativeOwnerPreserved(file, owner, afterOwner, home);
78
+ settleStage(stage, after, { versionVerified, ownerPreserved }, { currentTarget: afterOwner });
79
+ }
80
+ const rustup = path.join(home, '.cargo/bin/rustup');
81
+ if (fs.existsSync(rustup)) {
82
+ const stage = { owner: 'rust-toolchains', commands: [], state: 'running' }; result.stages.push(stage);
83
+ const before = commandOutput(run, rustup, ['toolchain', 'list'], { timeout: 30_000 }, stage); stage.before = before;
84
+ const channels = before.split('\n').map(l => l.split(/\s/)[0]).filter(n => /^(stable|nightly)(-|$)/.test(n));
85
+ if (!dryRun) for (const channel of channels) commandOutput(run, rustup, ['update', channel, '--no-self-update'], { timeout: 1_800_000 }, stage);
86
+ const after = commandOutput(run, rustup, ['toolchain', 'list'], { timeout: 30_000 }, stage);
87
+ const names = text => text.split('\n').filter(Boolean).map(line => line.split(/\s/)[0]);
88
+ settleStage(stage, after, { toolchainsPreserved: names(before).every(name => names(after).includes(name)) }, { channels });
89
+ result.exclusions.push('Pinned Rust toolchains and all local-source builds preserved');
90
+ }
91
+ }
92
+ if (config.homebrew && locate('brew')) {
93
+ const brew = required(locate('brew'));
94
+ const stage = { owner: 'homebrew-formulas', commands: [], state: 'running' }; result.stages.push(stage);
95
+ const snapshot = () => JSON.parse(commandOutput(run, brew, ['info', '--json=v2', '--installed'], { timeout: 120_000 }, stage));
96
+ const before = snapshot(); stage.before = before;
97
+ if (!dryRun) {
98
+ // HOMEBREW_NO_INSTALL_CLEANUP also set in coordinator child environment.
99
+ commandOutput(run, brew, ['update'], { timeout: 600_000 }, stage);
100
+ commandOutput(run, brew, ['upgrade', '--formula'], { timeout: 3_600_000 }, stage);
101
+ }
102
+ const after = snapshot();
103
+ const names = x => x.formulae.map(f => f.full_name).sort();
104
+ const currencyChecked = after.formulae.every(formula => typeof formula.outdated === 'boolean');
105
+ const pinned = after.formulae.filter(formula => formula.pinned === true).map(formula => formula.full_name);
106
+ const outdated = after.formulae.filter(formula => formula.outdated === true && formula.pinned !== true).map(formula => formula.full_name);
107
+ settleStage(stage, after, { formulaOwnersPreserved: names(before).every(name => names(after).includes(name)) }, { currencyChecked, pinned, outdated,
108
+ currency: currencyChecked ? outdated.length ? 'updates-available' : 'manager-current' : 'unverified' });
109
+ if (!currencyChecked) result.exclusions.push('Homebrew execution verified; formula currency markers unavailable');
110
+ for (const formula of pinned) result.exclusions.push(`Homebrew pinned formula preserved: ${formula}`);
111
+ result.exclusions.push('Homebrew casks need their application-specific owner (VS Code is the managed callback)');
112
+ }
113
+ if (config.uv && locate('uv')) {
114
+ const uv = required(locate('uv'));
115
+ const before = uvInventory(path.join(home, '.local/share/uv/tools'));
116
+ const stage = { owner: 'uv-registry-tools', before, state: 'running', commands: [] }; result.stages.push(stage);
117
+ if (!dryRun) for (const tool of before) if (tool.registry) commandOutput(run, uv, ['tool', 'upgrade', tool.name], { timeout: 900_000 }, stage);
118
+ const after = uvInventory(path.join(home, '.local/share/uv/tools'));
119
+ if (JSON.stringify(before.map(t => t.name).sort()) !== JSON.stringify(after.map(t => t.name).sort())) throw Error('uv tool install set changed');
120
+ for (const tool of before.filter(t => !t.registry)) {
121
+ if (after.find(t => t.name === tool.name)?.receipt !== tool.receipt) throw Error(`uv source changed: ${tool.name}`);
122
+ result.exclusions.push(`uv source/pin preserved: ${tool.name}`);
123
+ }
124
+ // Provider's --outdated result is captured rather than asserting public registry currency for local wheels.
125
+ const currency = commandOutput(run, uv, ['tool', 'list', '--outdated'], { timeout: 180_000 }, stage);
126
+ settleStage(stage, after, { toolSetPreserved: JSON.stringify(before.map(t => t.name).sort()) === JSON.stringify(after.map(t => t.name).sort()),
127
+ sourceRequirementsPreserved: before.filter(t => !t.registry).every(t => after.find(a => a.name === t.name)?.receipt === t.receipt), currencyChecked: typeof currency === 'string' }, { currency });
128
+ }
129
+ if (config.cargo && fs.existsSync(path.join(home, '.cargo/.crates2.json')) && locate('cargo')) {
130
+ const root = path.join(home, '.cargo'), cargo = required(locate('cargo'));
131
+ const before = cargoInventory(root);
132
+ const stage = { owner: 'cargo-registry-tools', before, state: 'running', commands: [], targets: [] }; result.stages.push(stage);
133
+ for (const tool of before) {
134
+ if (tool.source !== 'registry+https://github.com/rust-lang/crates.io-index' || tool.version_req) {
135
+ result.exclusions.push(`Cargo source/pin preserved: ${tool.name}`); continue;
136
+ }
137
+ for (const bin of tool.bins) required(path.join(root, 'bin', bin));
138
+ const response = await fetch(`https://crates.io/api/v1/crates/${encodeURIComponent(tool.name)}`, { signal: AbortSignal.timeout(45_000), headers: { 'User-Agent': 'ruvnet-brain/developer-update' } });
139
+ if (!response.ok) throw Error(`Cargo registry HTTP ${response.status}`);
140
+ const metadata = await response.json();
141
+ const target = metadata.crate?.max_stable_version;
142
+ if (!target) throw Error(`Cargo release unverified: ${tool.name}`);
143
+ stage.targets.push({ name: tool.name, target, registryOk: response.ok });
144
+ if (!dryRun && compare(target, tool.version) > 0) {
145
+ const args = ['install', tool.name, '--force', '--root', root];
146
+ if (tool.all_features) args.push('--all-features');
147
+ if (tool.no_default_features) args.push('--no-default-features');
148
+ if (tool.features?.length) args.push('--features', tool.features.join(','));
149
+ if (tool.target) args.push('--target', tool.target);
150
+ commandOutput(run, cargo, args, { timeout: 3_600_000 }, stage);
151
+ const live = cargoInventory(root).find(t => t.name === tool.name);
152
+ if (!live || compare(live.version, target) < 0 || compare(live.version, tool.version) < 0) throw Error(`Cargo update not verified: ${tool.name}`);
153
+ for (const bin of tool.bins) required(path.join(root, 'bin', bin));
154
+ }
155
+ }
156
+ const after = cargoInventory(root);
157
+ for (const tool of before.filter(t => !t.source.startsWith('registry+'))) {
158
+ const live = after.find(t => t.name === tool.name);
159
+ if (!live || live.version !== tool.version || live.source !== tool.source) throw Error(`Cargo local source changed: ${tool.name}`);
160
+ }
161
+ settleStage(stage, after, { registryTargetsVerified: stage.targets.every(t => t.registryOk && (dryRun || compare(after.find(a => a.name === t.name)?.version || '', t.target) >= 0)),
162
+ sourcesPreserved: before.filter(t => !t.source.startsWith('registry+')).every(t => after.some(a => a.name === t.name && a.version === t.version && a.source === t.source)),
163
+ noDowngrade: before.every(t => after.some(a => a.name === t.name && compare(a.version, t.version) >= 0)) });
164
+ }
165
+ if (config.managedCallback) {
166
+ const file = config.managedCallback;
167
+ if (typeof file !== 'string' || !path.isAbsolute(file) || ![path.join(home, '.codex/scripts/'), path.join(brainHome || home, 'scripts/')].some(root => file.startsWith(root)) || !file.endsWith('.mjs')) throw Error('managed callback must be an existing Codex scripts .mjs owner');
168
+ required(file);
169
+ const stage = { owner: file, state: 'running', commands: [] }; result.stages.push(stage);
170
+ const receipt = JSON.parse(commandOutput(run, node, [file, dryRun ? '--check' : '--apply'], { timeout: 3_600_000 }, stage));
171
+ if (receipt.ok !== true || !receipt.before || !receipt.after) throw Error('managed callback did not prove success with source snapshots');
172
+ stage.before = receipt.before;
173
+ settleStage(stage, receipt.after, { callbackSnapshotsPresent: !!receipt.before && !!receipt.after, callbackVerified: receipt.ok === true }, { receipt });
174
+ }
175
+ for (const name of ['homebrew', 'uv', 'cargo']) if (config[name] && !result.stages.some(s => s.owner.startsWith(name === 'homebrew' ? 'homebrew' : name))) result.exclusions.push(`${name} has no existing attributable owner/tool inventory`);
176
+ for (const name of ['homebrew', 'uv', 'cargo', 'native']) if (!config[name]) result.exclusions.push(`${name} maintenance disabled`);
177
+ return result;
178
+ }
@@ -0,0 +1,24 @@
1
+ // A configured coordinator owns automatic mutation. Its immutable scheduler module is the real door.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import crypto from 'node:crypto';
6
+ import { EXECUTION_MODULES } from './developer-update-policy.mjs';
7
+ const hash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
8
+ export function developerCoordinatorOwner({ home = os.homedir(), brainHome = process.env.RUVNET_BRAIN_HOME || path.join(home, '.cache/ruvnet-brain') } = {}) {
9
+ const config = path.join(brainHome, 'developer-update-config.json'), registration = path.join(brainHome, 'scheduler/registration.json');
10
+ let record;
11
+ try { record = JSON.parse(fs.readFileSync(registration, 'utf8')); } catch { /* handled below */ }
12
+ const configured = fs.existsSync(config) || record?.mode === 'developer-suite';
13
+ if (!configured) return { active: false, ready: false };
14
+ const entry = record?.updateModules?.['developer-update.mjs'];
15
+ try {
16
+ if (record?.mode !== 'developer-suite' || !path.isAbsolute(entry?.path || '') || !/^[a-f0-9]{64}$/.test(entry?.sha256 || '')
17
+ || hash(entry.path) !== entry.sha256 || hash(record.runnerPath) !== record.runnerSha256) throw Error('registered source is absent or changed');
18
+ if (!record.updateModules?.['plugin-artifact-proof.mjs'] && fs.readFileSync(entry.path, 'utf8').includes('./plugin-artifact-proof.mjs')) throw Error('new coordinator artifact closure is incomplete');
19
+ const generation = EXECUTION_MODULES.filter(name => name !== 'plugin-artifact-proof.mjs' || record.updateModules?.[name]);
20
+ if (generation.some(name => !record.updateModules?.[name])) throw Error('coordinator execution closure is incomplete');
21
+ for (const module of Object.values(record.updateModules)) if (hash(module.path) !== module.sha256) throw Error('registered module closure changed');
22
+ return { active: true, ready: true, entry: entry.path, node: record.nodePath, registration, sourceSha256: entry.sha256 };
23
+ } catch (error) { return { active: true, ready: false, reason: error.message }; }
24
+ }
@@ -0,0 +1,48 @@
1
+ export const EXECUTION_MODULES = Object.freeze(['developer-update.mjs', 'developer-update-policy.mjs', 'developer-update-lock.mjs', 'developer-update-maintenance.mjs', 'developer-update-cleanup.mjs', 'plugin-artifact-proof.mjs']);
2
+ // Shared stack classification and ordering: installations are selected, never manufactured.
3
+ export const FAMILY = /^(ruflo|ruvnet-brain|ruvector|ruvector-extensions|ruvi|ruvbot|qudag|flow-nexus|agent-browser|agent-browser-mcp|agentic-flow|agentic-qe|agentic-robotics|agentic-payments|agentdb|ruv-swarm|@pacphi\/agentic-kit$|@ruvector\/|@claude-flow\/|@metaharness\/|@agentic-robotics\/)/;
4
+ export const PLUGIN_MARKETPLACES = new Set(['ruflo', 'ruview', 'ruvnet-brain', 'cognitum']);
5
+ export const REVIEWED_INSTALL_SCRIPTS = Object.freeze(['ruflo', 'agentic-qe', '@claude-flow/cli', 'better-sqlite3', 'hnswlib-node', 'agentdb', 'agentic-flow', 'argon2', 'onnxruntime-node', 'sharp', 'protobufjs', '@google/genai', 'tldjs', 'vibium', 'agent-browser', '@anthropic-ai/claude-code', 'opencode-ai']);
6
+ export function cmpVersion(a, b) {
7
+ const parse = value => {
8
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?(?:\+[\w.-]+)?$/.exec(value);
9
+ if (!m) throw Error(`uncomparable version: ${value}`);
10
+ return [m.slice(1, 4).map(Number), m[4]?.split('.')];
11
+ };
12
+ const [ac, ap] = parse(a), [bc, bp] = parse(b);
13
+ for (let i = 0; i < 3; i++) if (ac[i] !== bc[i]) return Math.sign(ac[i] - bc[i]);
14
+ if (!ap || !bp) return ap ? -1 : bp ? 1 : 0;
15
+ for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
16
+ if (ap[i] === undefined || bp[i] === undefined) return ap[i] === undefined ? -1 : 1;
17
+ if (ap[i] === bp[i]) continue;
18
+ const an = /^\d+$/.test(ap[i]), bn = /^\d+$/.test(bp[i]);
19
+ return an && bn ? Math.sign(Number(ap[i]) - Number(bp[i])) : an !== bn ? an ? -1 : 1 : ap[i] < bp[i] ? -1 : 1;
20
+ }
21
+ return 0;
22
+ }
23
+ export function normalizeNpmDistTags(value) {
24
+ // npm 11 emits an object for a queried field; npm 12 retains its singleton record array.
25
+ if (Array.isArray(value)) {
26
+ if (value.length !== 1) throw Error('npm dist-tags has ambiguous or empty records');
27
+ [value] = value;
28
+ }
29
+ if (!value || typeof value !== 'object' || Array.isArray(value)
30
+ || !Object.keys(value).length || Object.values(value).some(version => typeof version !== 'string' || !version)) {
31
+ throw Error('npm dist-tags record is unverified');
32
+ }
33
+ return value;
34
+ }
35
+ export function pickTargetTag(tags, want = 'latest', defaultTag = 'latest') {
36
+ const candidates = [...new Set([want, defaultTag])].filter(tag => typeof tags?.[tag] === 'string');
37
+ candidates.sort((a, b) => cmpVersion(tags[b], tags[a]));
38
+ return { tag: candidates[0] || null, target: tags?.[candidates[0]] || null };
39
+ }
40
+ export function selectTag(name, current, tags, channel = 'latest') {
41
+ if (!['latest', 'alpha'].includes(channel)) throw Error('unsupported update channel');
42
+ // Kit's installed release contract uses next; never convert it to an npm alpha alias.
43
+ tags = normalizeNpmDistTags(tags);
44
+ const wanted = name === '@pacphi/agentic-kit' ? 'next' : FAMILY.test(name) && channel === 'alpha' ? 'alpha' : 'latest';
45
+ const { tag, target: version } = pickTargetTag(tags, wanted);
46
+ if (!tag) throw Error(`no allowed release tag: ${name}`);
47
+ return { tag, version, upgrade: cmpVersion(version, current) > 0, ahead: cmpVersion(version, current) < 0 };
48
+ }