ruvnet-brain 4.7.17 → 4.7.18
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 +2 -2
- package/bin/install.mjs +82 -15
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/agentdb-recall-process.mjs +1 -1
- package/plugin/scripts/agentdb-recall-reader.mjs +1 -1
- package/plugin/scripts/brain-confirmation.mjs +8 -1
- package/plugin/scripts/brain-footprint.mjs +3 -1
- package/plugin/scripts/decision-record.mjs +1 -1
- package/plugin/scripts/degradation-watch.mjs +13 -1
- package/plugin/scripts/developer-update-policy.mjs +31 -5
- package/plugin/scripts/developer-update.mjs +17 -5
- package/plugin/scripts/learning-store.mjs +1 -1
- package/plugin/scripts/ruflo-bin.mjs +5 -1
- package/plugin/scripts/ruflo-memory-hooks.mjs +1 -1
- package/plugin/scripts/turn-outcome-capture.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +1 -1
- package/scripts/corpus-dispatch-decision.mjs +5 -4
- package/scripts/distill-project.mjs +1 -1
- package/scripts/falsify.mjs +1 -1
- package/scripts/health-repair.mjs +1 -1
- package/scripts/learning-replay-execution.mjs +3 -3
- package/scripts/learning-replay-fixture.mjs +3 -0
- package/scripts/lesson-migrate-agentdb.mjs +1 -1
- package/scripts/nightly-wrapper.sh +2 -2
- package/scripts/proxy/claude-proxied.sh +1 -0
- package/scripts/proxy/proxy-revert.sh +1 -0
- package/scripts/proxy/proxy-up.sh +1 -0
- package/scripts/proxy/proxy-verify.mjs +1 -1
- package/scripts/record-lesson.mjs +1 -1
- package/scripts/selfcheck.mjs +1 -1
- package/scripts/single-source-check.mjs +1 -1
- package/scripts/stack-sync.mjs +11 -3
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
|
-
[](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
|
|
|
@@ -255,7 +255,7 @@ uses the protected release workflow; a consumer update downloads an available ge
|
|
|
255
255
|
does not rebuild upstream knowledge. Check `--doctor`, `SOURCE.json` and refresh receipts
|
|
256
256
|
before treating an installation as current.
|
|
257
257
|
|
|
258
|
-
The **4.6.0 published-corpus baseline** is **205 public stores · 165,
|
|
258
|
+
The **4.6.0 published-corpus baseline** is **205 public stores · 165,488 public source chunks**.
|
|
259
259
|
Those are bundle census values, not a claim that every upstream repository is fresh today.
|
|
260
260
|
Later corpus publication may advance the knowledge generation independently of code changes.
|
|
261
261
|
|
package/bin/install.mjs
CHANGED
|
@@ -2028,25 +2028,53 @@ export function codexMarketplaceRows(value) {
|
|
|
2028
2028
|
return Array.isArray(value) ? value : [];
|
|
2029
2029
|
}
|
|
2030
2030
|
|
|
2031
|
-
|
|
2031
|
+
// Codex refuses EVERY plugin command while any configured marketplace snapshot fails to load, including one that is
|
|
2032
|
+
// not the Brain's. Measured 2026-10-23: a 7:21 p.m. ET update failed host convergence on "`claude-code-plugins` at
|
|
2033
|
+
// ~/.codex/.tmp/marketplaces/claude-code-plugins: marketplace root does not contain a supported manifest" while Codex
|
|
2034
|
+
// was refreshing that Git snapshot; the same step passed at 5 a.m. Reproduced with a snapshot missing its manifest:
|
|
2035
|
+
// `codex plugin list` exits 1, and `codex plugin marketplace upgrade <name>` repairs it (codex-cli 0.164.0).
|
|
2036
|
+
const CODEX_SNAPSHOT_FAILURE = /failed to load configured marketplace snapshot/i;
|
|
2037
|
+
export function codexBrokenMarketplaces(text) {
|
|
2038
|
+
return [...String(text || '').matchAll(/^- `([^`]+)` at /gm)].map((m) => m[1]);
|
|
2039
|
+
}
|
|
2040
|
+
const sleepSync = (ms) => { if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); };
|
|
2041
|
+
|
|
2042
|
+
export function runCodexJson(args, {
|
|
2032
2043
|
codexBin = process.env.CODEX_BIN || 'codex',
|
|
2033
2044
|
codexHome = codexHomeDir(),
|
|
2034
2045
|
cwd = process.cwd(),
|
|
2046
|
+
run = runHostCli,
|
|
2047
|
+
waitMs = 5_000,
|
|
2048
|
+
// Refreshing another tool's marketplace is a network write; only an install/update that is about to change the
|
|
2049
|
+
// Brain's Codex plugin opts in. --doctor and listings report the broken snapshot and change nothing (review 2026-10-24).
|
|
2050
|
+
repair = false,
|
|
2035
2051
|
} = {}) {
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2052
|
+
const options = { stdio: 'pipe', shell: false, cwd, env: { ...process.env, CODEX_HOME: codexHome }, encoding: 'utf8',
|
|
2053
|
+
timeout: 30_000, maxBuffer: 20 * 1024 * 1024 };
|
|
2054
|
+
const snapshotFailure = (res) => !res.missingBinary && res.status !== 0 && CODEX_SNAPSHOT_FAILURE.test(String(res.stderr || res.stdout || ''));
|
|
2055
|
+
let r = run(codexBin, args, options);
|
|
2056
|
+
// One wait covers a refresh Codex is finishing; then refresh only the OTHER Git marketplaces it named (the Brain's
|
|
2057
|
+
// own local snapshot is repaired by prepareCodexMarketplace), once, and ask again. Bounded: at most three asks.
|
|
2058
|
+
const repaired = [];
|
|
2059
|
+
if (snapshotFailure(r)) { sleepSync(waitMs); r = run(codexBin, args, options); }
|
|
2060
|
+
if (repair && snapshotFailure(r)) {
|
|
2061
|
+
for (const name of codexBrokenMarketplaces(r.stderr || r.stdout).filter((n) => n !== CODEX_MARKETPLACE)) {
|
|
2062
|
+
const up = run(codexBin, ['plugin', 'marketplace', 'upgrade', name, '--json'], options);
|
|
2063
|
+
repaired.push(`${name}: ${up.status === 0 ? 'refreshed' : `refresh failed (exit ${up.status})`}`);
|
|
2064
|
+
}
|
|
2065
|
+
if (repaired.length) r = run(codexBin, args, options);
|
|
2066
|
+
}
|
|
2045
2067
|
if (r.missingBinary) return { ok: false, error: r.message };
|
|
2046
2068
|
if (r.error || r.status !== 0) {
|
|
2047
|
-
|
|
2048
|
-
|
|
2069
|
+
let detail = String(r.stderr || r.stdout || r.error?.message || `exit ${r.status}`).trim();
|
|
2070
|
+
if (snapshotFailure(r)) {
|
|
2071
|
+
const names = codexBrokenMarketplaces(r.stderr || r.stdout);
|
|
2072
|
+
detail += ` — Codex cannot load its ${names.map((n) => `\`${n}\``).join(', ') || 'configured'} marketplace snapshot`
|
|
2073
|
+
+ `${repaired.length ? ` (tried: ${repaired.join('; ')})` : ''}; repair with: ${names.map((n) => `codex plugin marketplace upgrade ${n}`).join(' && ') || 'codex plugin marketplace upgrade'}`;
|
|
2074
|
+
}
|
|
2075
|
+
return { ok: false, error: detail, ...(repaired.length ? { repaired } : {}) };
|
|
2049
2076
|
}
|
|
2077
|
+
if (repaired.length && !TEST_MODE) info(` Codex marketplace snapshot repaired before the Brain step (${repaired.join('; ')})`);
|
|
2050
2078
|
try { return { ok: true, value: JSON.parse(r.stdout || 'null') }; }
|
|
2051
2079
|
catch { return { ok: false, error: `Codex returned non-JSON output for ${args.join(' ')}` }; }
|
|
2052
2080
|
}
|
|
@@ -2105,7 +2133,7 @@ export function wireCodexPlugin({
|
|
|
2105
2133
|
aliasHome,
|
|
2106
2134
|
} = {}) {
|
|
2107
2135
|
if (!fs.existsSync(codexDir)) return { host: false, action: 'no-host' };
|
|
2108
|
-
const options = { codexBin, codexHome, cwd };
|
|
2136
|
+
const options = { codexBin, codexHome, cwd, repair: true };
|
|
2109
2137
|
// Codex loads every configured marketplace before it can report plugin state. Repair only an
|
|
2110
2138
|
// absent/malformed Brain-owned snapshot before that probe, or the probe can fail before its own
|
|
2111
2139
|
// repair path is reachable. A healthy current cache stays byte-untouched, and plugin enablement
|
|
@@ -3874,7 +3902,9 @@ export function syncHostsAfterUpdate(cacheDir = resolvedKbDir(), {
|
|
|
3874
3902
|
const fail = (detail = {}) => {
|
|
3875
3903
|
runtimeTransaction.rollback();
|
|
3876
3904
|
hostLock.release();
|
|
3877
|
-
|
|
3905
|
+
// Three paths below returned no error at all, so the refresh receipt recorded `state: null, error: null` and the
|
|
3906
|
+
// failure could not be diagnosed afterwards (2026-10-23 7:21 p.m. ET). Every failure now carries its reason.
|
|
3907
|
+
return { ok: false, results, ...detail, error: detail.error || hostFailureReason(results) };
|
|
3878
3908
|
};
|
|
3879
3909
|
|
|
3880
3910
|
try {
|
|
@@ -4035,6 +4065,18 @@ export function classifyHostConvergence(receipt, expectedVersion = PACKAGE_VERSI
|
|
|
4035
4065
|
}
|
|
4036
4066
|
|
|
4037
4067
|
/** Activation and host readiness are independent; never claim a rollback from a host refusal. */
|
|
4068
|
+
/** Why host synchronization stopped, from the per-host results it had gathered; null when nothing says. */
|
|
4069
|
+
export function hostFailureReason(results = {}) {
|
|
4070
|
+
const firstLines = (text) => String(text || '').trim().split('\n').slice(0, 3).join(' ');
|
|
4071
|
+
if (results.claude?.host && !results.claude.wired) {
|
|
4072
|
+
return `Claude Code plugin ${results.claude.action || 'wiring failed'}${results.claude.version ? ` (installed ${results.claude.version}, required ${results.claude.expectedVersion || PACKAGE_VERSION})` : ''}`;
|
|
4073
|
+
}
|
|
4074
|
+
if (results.codex && !['unchanged', 'installed', 'updated', 'disabled'].includes(results.codex.action)) {
|
|
4075
|
+
return `Codex plugin ${results.codex.action || 'failed'}${results.codex.error ? `: ${firstLines(results.codex.error)}` : ''}`;
|
|
4076
|
+
}
|
|
4077
|
+
return null;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4038
4080
|
export function hostSynchronizationFailureMessage(convergence, active, expectedVersion = PACKAGE_VERSION) {
|
|
4039
4081
|
const verified = active?.version === expectedVersion && Number.isSafeInteger(active.generation) && active.generation > 0
|
|
4040
4082
|
&& active.codeRoot === `versions/${expectedVersion}`;
|
|
@@ -4093,11 +4135,36 @@ function refuseOverSetAsideBrain() {
|
|
|
4093
4135
|
return true;
|
|
4094
4136
|
}
|
|
4095
4137
|
|
|
4138
|
+
/**
|
|
4139
|
+
* A SOURCE CHECKOUT older than the runtime already installed cannot apply the current release: it downloads ~736 MB
|
|
4140
|
+
* and then refuses on "staged ReleaseCoverage failed integrity — release version 4.7.17 differs from 4.5.2" (measured
|
|
4141
|
+
* 2026-10-23: `npx ruvnet-brain --update` run inside a stale checkout resolves that checkout's own bin). Say so
|
|
4142
|
+
* before the download, with the command that works. Only a checkout: a published installer can legitimately be
|
|
4143
|
+
* older than active.json (a plugin seeded from main during a release window, a release candidate installed from a
|
|
4144
|
+
* checkout), and the nightly and self-heal `@latest` updates must keep working then (review, 2026-10-24).
|
|
4145
|
+
*/
|
|
4146
|
+
export function olderInstallerRefusal({ brainHome, packageVersion = PACKAGE_VERSION, repoRoot = REPO_ROOT } = {}) {
|
|
4147
|
+
let installed;
|
|
4148
|
+
try { installed = JSON.parse(fs.readFileSync(path.join(brainHome, 'active.json'), 'utf8'))?.version; } catch { return null; }
|
|
4149
|
+
if (!/^\d+\.\d+\.\d+$/.test(String(installed || ''))) return null;
|
|
4150
|
+
try { if (cmpVersion(installed, packageVersion) <= 0) return null; } catch { return null; }
|
|
4151
|
+
if (!fs.existsSync(path.join(repoRoot, '.git'))) return null;
|
|
4152
|
+
return `this is ruvnet-brain ${packageVersion}, a source checkout at ${repoRoot}, older than the ${installed} already `
|
|
4153
|
+
+ 'installed here, and an older checkout cannot apply the current release. '
|
|
4154
|
+
+ 'Run npx ruvnet-brain@latest --update from any other folder, or bring this checkout up to date first (git pull).';
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4096
4157
|
async function runUpdate() {
|
|
4097
4158
|
printBanner('update');
|
|
4098
4159
|
refuseUnmountedBrain();
|
|
4099
4160
|
const kbDir = resolvedKbDir();
|
|
4100
4161
|
const brainHome = process.env.RUVNET_BRAIN_HOME || path.dirname(kbDir);
|
|
4162
|
+
const olderInstaller = olderInstallerRefusal({ brainHome });
|
|
4163
|
+
if (olderInstaller) {
|
|
4164
|
+
console.error(`\n${c.red('✗ update stopped before downloading:')} ${olderInstaller}\n`);
|
|
4165
|
+
process.exitCode = 1;
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4101
4168
|
if (FLAG_HOST_SYNC_ONLY) {
|
|
4102
4169
|
info(c.dim('repairing host shells and managed model catalog without mutating KB bytes…\n'));
|
|
4103
4170
|
const convergence = syncHostsAfterUpdate(kbDir);
|
|
@@ -5650,7 +5717,7 @@ const rufloProbeRun = (args, cli = 'ruflo', timeoutMs = 10_000) => {
|
|
|
5650
5717
|
// (verified live: ~/.npm-global/lib/node_modules/ruflo/node_modules/@claude-flow/cli/dist/src/
|
|
5651
5718
|
// services/daemon-autostart.js:85) — a read-only health probe must not leave one running.
|
|
5652
5719
|
const result = spawnSync(cli, args, { cwd: process.cwd(), encoding: 'utf8', timeout: timeoutMs, shell: IS_WIN && /\.(?:cmd|bat)$/i.test(cli),
|
|
5653
|
-
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } });
|
|
5720
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' } });
|
|
5654
5721
|
// A spawn failure or a timeout is marked, so it can never pass for an (empty, "healthy") answer.
|
|
5655
5722
|
return `${result.stdout || ''}\n${result.stderr || ''}${result.error ? `\n[ruflo-probe-error] ${result.error.code || result.error.message}` : ''}`;
|
|
5656
5723
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.18",
|
|
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.
|
|
4
|
+
"version": "4.7.18",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -33,7 +33,7 @@ export function searchOnce({ ruflo, store, args, deadline, env, scratch, operati
|
|
|
33
33
|
catch { cleanup(); resolve({ rows: [], state: 'unavailable' }); return; }
|
|
34
34
|
let child;
|
|
35
35
|
try {
|
|
36
|
-
child = spawn(inv.executable, inv.args, { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_MEMORY_PATH: undefined },
|
|
36
|
+
child = spawn(inv.executable, inv.args, { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false', CLAUDE_FLOW_MEMORY_PATH: undefined },
|
|
37
37
|
stdio: ['ignore', 'pipe', 'ignore'], detached: process.platform !== 'win32', windowsHide: true });
|
|
38
38
|
} catch { cleanup(); resolve({ rows: [], state: 'unavailable' }); return; }
|
|
39
39
|
let out = '', outputBytes = 0;
|
|
@@ -8,7 +8,7 @@ import { resolveRuflo, rufloInvocationOrPlain } from './ruflo-bin.mjs';
|
|
|
8
8
|
const call = (bin, args, deadline) => new Promise((resolve) => {
|
|
9
9
|
const timeout = Math.max(1, deadline - Date.now());
|
|
10
10
|
const inv = rufloInvocationOrPlain(bin, args, { env: process.env });
|
|
11
|
-
execFile(inv.executable, inv.args, { timeout, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } },
|
|
11
|
+
execFile(inv.executable, inv.args, { timeout, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' } },
|
|
12
12
|
(error, stdout) => resolve(error ? null : String(stdout || '')));
|
|
13
13
|
});
|
|
14
14
|
|
|
@@ -181,8 +181,15 @@ export function confirm({ footprint, env = process.env, home = os.homedir(), now
|
|
|
181
181
|
// Footprint
|
|
182
182
|
const parts = Object.entries(footprint.breakdown).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k} ${formatBytes(v)}`);
|
|
183
183
|
const foreign = footprint.unowned.reduce((n, i) => n + (i.bytes || 0), 0);
|
|
184
|
+
// `--clean` is the fix only when it can remove something. Measured 2026-10-24: 3.01 GB of a 2.23 GB budget with
|
|
185
|
+
// zero cruft, the line said "fix: --clean", and --clean freed nothing — the bytes were a 1.3 GB npm install ruflo's
|
|
186
|
+
// self-update left in ruvnet-brain/ruflo-cwd, counted as Brain state. Name where the bytes are instead.
|
|
187
|
+
const reclaimable = footprint.cruft.some((i) => ['remove', 'remove-if-proven', 'rotate', 'truncate', 'collect', 'prune'].includes(i.action));
|
|
188
|
+
const largest = footprint.items.filter((i) => i.kind === 'state' && i.bytes).sort((a, b) => b.bytes - a.bytes).slice(0, 2);
|
|
189
|
+
const footprintFix = reclaimable || !largest.length ? CLEAN
|
|
190
|
+
: `${CLEAN} has nothing to remove; the largest Brain folders are ${largest.map((i) => `${i.path} (${formatBytes(i.bytes)})`).join(' and ')} — see what is filling them`;
|
|
184
191
|
lines.push(line('footprint', 'Footprint', footprint.withinBudget ? 'ok' : 'fail',
|
|
185
|
-
`${formatBytes(footprint.totalBytes)} of ${formatBytes(footprint.budgetBytes)} budget (${parts.join(' · ')})${foreign ? `; not counted: ${formatBytes(foreign)} owned by other tools` : ''}`,
|
|
192
|
+
`${formatBytes(footprint.totalBytes)} of ${formatBytes(footprint.budgetBytes)} budget (${parts.join(' · ')})${foreign ? `; not counted: ${formatBytes(foreign)} owned by other tools` : ''}`, footprintFix));
|
|
186
193
|
|
|
187
194
|
// No cruft
|
|
188
195
|
const cruft = footprint.cruft;
|
|
@@ -25,6 +25,7 @@ import path from 'node:path';
|
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
26
|
import { isKbTree, kbCopyProof } from './kb-copy-proof.mjs';
|
|
27
27
|
import { brainLocation } from './brain-location.mjs';
|
|
28
|
+
import { RUFLO_CWD_ARTIFACTS } from './ruflo-bin.mjs';
|
|
28
29
|
import { assessMoveLeftovers, cachedKept, cmpVersion, isVolumeMetadata, keptCopyFix, physical, pidAlive, readProofCache, rememberKept, removeWithin, rotate,
|
|
29
30
|
treeBytes, truncateToTail } from './footprint-io.mjs';
|
|
30
31
|
|
|
@@ -59,7 +60,8 @@ export const LOG_FILES = Object.freeze(['evidence.jsonl', 'token-ledger.jsonl',
|
|
|
59
60
|
'capability-live-evidence.jsonl', 'design-grades.jsonl', 'grounding-overrides.jsonl']);
|
|
60
61
|
const TEXT_LOG = /^\.(?:last-[A-Za-z0-9-]+|seed)\.log$/;
|
|
61
62
|
const RECOVERY_LEFTOVER = /(?:\.(?:bak|retired|dead)-\d{6,}|^bootstrap-backup-\d)/;
|
|
62
|
-
|
|
63
|
+
// One list with ruflo-bin.mjs, which clears the same names before each call (incl. a self-update's npm install, 1.3 GB on 2026-10-22).
|
|
64
|
+
const RUFLO_DEBRIS = new Set(RUFLO_CWD_ARTIFACTS);
|
|
63
65
|
// Directories in the brain home that hold data another tool owns: measured, shown, never budgeted or removed.
|
|
64
66
|
const FOREIGN = { 'ruvector-mcp': 'RuVector MCP server working store (its own live memory)',
|
|
65
67
|
'worktree-recovery': 'hand-made recovery archives, not created by the Brain' };
|
|
@@ -138,7 +138,7 @@ function runRuflo(ruflo, db, args, { spawn = spawnSync } = {}) {
|
|
|
138
138
|
try {
|
|
139
139
|
const { executable, args: argv } = rufloInvocation(ruflo, args);
|
|
140
140
|
const r = spawn(executable, argv, { cwd, encoding: 'utf8', timeout: 60_000, windowsHide: true,
|
|
141
|
-
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_MEMORY_PATH: undefined } });
|
|
141
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false', CLAUDE_FLOW_MEMORY_PATH: undefined } });
|
|
142
142
|
return { status: Number.isInteger(r.status) ? r.status : 1, stdout: String(r.stdout || ''), output: `${r.stderr || ''}\n${r.stdout || ''}${r.error ? `\n${r.error.message}` : ''}` };
|
|
143
143
|
} finally { fs.rmSync(cwd, { recursive: true, force: true }); }
|
|
144
144
|
}
|
|
@@ -52,6 +52,7 @@ import path from 'node:path';
|
|
|
52
52
|
// is the exact defect class this file exists to close. Caught by counting occurrences, not by
|
|
53
53
|
// reading the code and believing it.
|
|
54
54
|
import { fileURLToPath } from 'node:url';
|
|
55
|
+
import { rufloRunDir } from './ruflo-bin.mjs';
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* THE PROBE TARGET IS A SCRATCH DB, NOT THE PROJECT'S.
|
|
@@ -73,8 +74,19 @@ import { fileURLToPath } from 'node:url';
|
|
|
73
74
|
*/
|
|
74
75
|
const defaultDb = () => path.join(os.tmpdir(), `ruvnet-durability-probe-${process.pid}.db`);
|
|
75
76
|
|
|
77
|
+
// ruflo writes into its cwd on every call (.claude/, .claude-flow/, ruvector.db, agentdb.rvf) and its startup
|
|
78
|
+
// self-update runs `npm install @claude-flow/cli --save-exact` there. This probe runs on every `git push`, so with the
|
|
79
|
+
// caller's cwd it could leave those in the user's project (measured 2026-10-22: a 1.4 GB node_modules and a
|
|
80
|
+
// package.json in a project). It now runs in a private scratch dir, removed after the call, with both switches off.
|
|
76
81
|
function defaultRun(cmd, args) {
|
|
77
|
-
|
|
82
|
+
let cwd = null;
|
|
83
|
+
try { cwd = rufloRunDir(path.join(os.tmpdir(), 'ruvnet-durability-probe')); } catch { /* fall back to tmp below */ }
|
|
84
|
+
try {
|
|
85
|
+
return execFileSync(cmd, args, { cwd: cwd || os.tmpdir(), encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
86
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' } });
|
|
87
|
+
} finally {
|
|
88
|
+
if (cwd) { try { fs.rmSync(cwd, { recursive: true, force: true }); } catch { /* swept later as a stale run- dir */ } }
|
|
89
|
+
}
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/**
|
|
@@ -32,17 +32,43 @@ export function normalizeNpmDistTags(value) {
|
|
|
32
32
|
}
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
-
export function
|
|
36
|
-
|
|
35
|
+
export function normalizeNpmTimes(value) {
|
|
36
|
+
// `npm view <pkg> time --json`: an object of version -> ISO time (npm 12 wraps it in a one-record array).
|
|
37
|
+
if (Array.isArray(value)) value = value.length === 1 ? value[0] : null;
|
|
38
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The newer of {want, defaultTag} by version — EXCEPT a non-default tag whose version was published BEFORE the
|
|
42
|
+
* default tag's version. That is an abandoned pre-release line, not a newer build: measured 2026-10-23, agentic-flow
|
|
43
|
+
* `alpha` = 3.0.0-alpha.2 (published 2026-03-25) against `latest` = 2.1.4 (2026-10-09); by version alone the alpha
|
|
44
|
+
* channel would install seven-month-old code. `published` is `npm view <pkg> time`; without it, ordering is unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export function pickTargetTag(tags, want = 'latest', defaultTag = 'latest', { published = null } = {}) {
|
|
47
|
+
const at = (version) => Date.parse(published?.[version] ?? '');
|
|
48
|
+
// `published` null: the caller did not ask, ordering is by version. An object: times were REQUIRED, so a missing or
|
|
49
|
+
// unreadable time makes the non-default tag unverifiable and it is skipped (cross-model review 2026-10-24: a failed
|
|
50
|
+
// lookup must not let a seven-month-old pre-release through).
|
|
51
|
+
const abandoned = (tag) => tag !== defaultTag && typeof tags?.[defaultTag] === 'string' && published !== null
|
|
52
|
+
&& (!Number.isFinite(at(tags[tag])) || !Number.isFinite(at(tags[defaultTag])) || at(tags[tag]) < at(tags[defaultTag]));
|
|
53
|
+
const candidates = [...new Set([want, defaultTag])].filter(tag => typeof tags?.[tag] === 'string' && !abandoned(tag));
|
|
37
54
|
candidates.sort((a, b) => cmpVersion(tags[b], tags[a]));
|
|
38
55
|
return { tag: candidates[0] || null, target: tags?.[candidates[0]] || null };
|
|
39
56
|
}
|
|
40
|
-
|
|
57
|
+
/** Whether choosing between `want` and `latest` needs publish times: only when the two tags name different versions. */
|
|
58
|
+
export function needsPublishTimes(tags, want) {
|
|
59
|
+
return want !== 'latest' && typeof tags?.[want] === 'string' && typeof tags?.latest === 'string' && tags[want] !== tags.latest;
|
|
60
|
+
}
|
|
61
|
+
/** The ONE answer to "which tag does this package track": shared by the updater and the stack-sync notice. */
|
|
62
|
+
export function wantedTag(name, channel = 'latest') {
|
|
41
63
|
if (!['latest', 'alpha'].includes(channel)) throw Error('unsupported update channel');
|
|
42
64
|
// Kit's installed release contract uses next; never convert it to an npm alpha alias.
|
|
65
|
+
return name === '@pacphi/agentic-kit' ? 'next' : FAMILY.test(name) && channel === 'alpha' ? 'alpha' : 'latest';
|
|
66
|
+
}
|
|
67
|
+
export function selectTag(name, current, tags, channel = 'latest', { published = null } = {}) {
|
|
68
|
+
const wanted = wantedTag(name, channel);
|
|
43
69
|
tags = normalizeNpmDistTags(tags);
|
|
44
|
-
const
|
|
45
|
-
const { tag, target: version } = pickTargetTag(tags, wanted);
|
|
70
|
+
const times = published === null || published === undefined ? null : (normalizeNpmTimes(published) || {});
|
|
71
|
+
const { tag, target: version } = pickTargetTag(tags, wanted, 'latest', { published: times });
|
|
46
72
|
if (!tag) throw Error(`no allowed release tag: ${name}`);
|
|
47
73
|
return { tag, version, upgrade: cmpVersion(version, current) > 0, ahead: cmpVersion(version, current) < 0 };
|
|
48
74
|
}
|
|
@@ -6,7 +6,7 @@ import os from 'node:os';
|
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
|
-
import { cmpVersion as compare, selectTag, FAMILY, PLUGIN_MARKETPLACES, REVIEWED_INSTALL_SCRIPTS, EXECUTION_MODULES } from './developer-update-policy.mjs';
|
|
9
|
+
import { cmpVersion as compare, selectTag, needsPublishTimes, normalizeNpmDistTags, wantedTag, FAMILY, PLUGIN_MARKETPLACES, REVIEWED_INSTALL_SCRIPTS, EXECUTION_MODULES } from './developer-update-policy.mjs';
|
|
10
10
|
import { acquireDeveloperLock, sharedLockStatus } from './developer-update-lock.mjs';
|
|
11
11
|
import { maintenance } from './developer-update-maintenance.mjs';
|
|
12
12
|
import { cleanupNpxDuplicates } from './developer-update-cleanup.mjs';
|
|
@@ -31,7 +31,7 @@ export function atomic(file, value) {
|
|
|
31
31
|
function invoke(command, args, { cwd = HOME, timeout = 600_000, allowed = [0], env = process.env, capture = false } = {}) {
|
|
32
32
|
const r = spawnSync(command, args, { cwd, encoding: 'utf8', timeout, maxBuffer: 16 * 1024 * 1024,
|
|
33
33
|
env: { ...env, PATH: [...new Set([path.dirname(NODE), path.join(env.npm_config_prefix || PREFIX, process.platform === 'win32' ? '' : 'bin'), ...(env.PATH || '').split(path.delimiter)])].join(path.delimiter),
|
|
34
|
-
CI: '1', DISABLE_AUTOUPDATER: '1', GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'Never', GH_PROMPT_DISABLED: '1', HOMEBREW_NO_INSTALL_CLEANUP: '1', HOMEBREW_NO_AUTO_UPDATE: '1', RUVNET_BRAIN_HOME: env.RUVNET_BRAIN_HOME || CACHE } });
|
|
34
|
+
CI: '1', DISABLE_AUTOUPDATER: '1', CLAUDE_FLOW_AUTO_UPDATE: 'false', GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'Never', GH_PROMPT_DISABLED: '1', HOMEBREW_NO_INSTALL_CLEANUP: '1', HOMEBREW_NO_AUTO_UPDATE: '1', RUVNET_BRAIN_HOME: env.RUVNET_BRAIN_HOME || CACHE } });
|
|
35
35
|
if (!capture && (r.error || !allowed.includes(r.status))) throw Error(`${path.basename(command)} ${args[0]}: ${r.error?.message || `exit ${r.status}`}: ${(r.stderr || r.stdout || '').trim().slice(-600)}`);
|
|
36
36
|
return { stdout: (r.stdout || '').trim(), stderr: (r.stderr || '').trim(), exitCode: r.status, error: r.error?.message || null };
|
|
37
37
|
}
|
|
@@ -120,7 +120,7 @@ function verifyPackage(before, expected, run) {
|
|
|
120
120
|
}
|
|
121
121
|
return after;
|
|
122
122
|
}
|
|
123
|
-
export function upgradePackage(before, tags, scripts = REVIEWED_INSTALL_SCRIPTS, { run = invokeText, dryRun = false, channel = 'latest', npm = NPM, preservePackages = [] } = {}) {
|
|
123
|
+
export function upgradePackage(before, tags, scripts = REVIEWED_INSTALL_SCRIPTS, { run = invokeText, dryRun = false, channel = 'latest', npm = NPM, preservePackages = [], published = null } = {}) {
|
|
124
124
|
// Snapshot identity must still exist and own the original paths before any installer runs.
|
|
125
125
|
if (before.localSource && before.rawLink !== undefined) {
|
|
126
126
|
if (!fs.lstatSync(before.location).isSymbolicLink() || fs.readlinkSync(before.location) !== before.rawLink) throw Error(`source link changed before preservation: ${before.name}`);
|
|
@@ -133,7 +133,7 @@ export function upgradePackage(before, tags, scripts = REVIEWED_INSTALL_SCRIPTS,
|
|
|
133
133
|
if (live.localSource) return { name: before.name, state: 'local-source-preserved', before, after: live };
|
|
134
134
|
if (live.launchers.some(b => !b.real)) throw Error(`absent launcher: ${before.name}`);
|
|
135
135
|
if (live.launchers.some(b => !b.owned)) return { name: before.name, state: 'shadowed-preserved', before, after: live };
|
|
136
|
-
const target = selectTag(before.name, before.version, tags, channel);
|
|
136
|
+
const target = selectTag(before.name, before.version, tags, channel, { published });
|
|
137
137
|
if (preservePackages.includes(before.name)) return { name: before.name, state: 'local-modification-preserved', reason: 'preservedLocalModification', target, before, after: live };
|
|
138
138
|
if (!target.upgrade || dryRun) return { name: before.name, state: target.upgrade ? 'update-available' : target.ahead ? 'ahead-preserved' : 'current', target, before, after: live };
|
|
139
139
|
const spec = `${before.name}@${target.tag}`;
|
|
@@ -422,7 +422,19 @@ export async function runDeveloperUpdate({ mode = 'check', home = os.homedir(),
|
|
|
422
422
|
persistReceipt();
|
|
423
423
|
for (const before of receipt.before) {
|
|
424
424
|
const tags = before.localSource ? {} : JSON.parse(run(npm, ['view', before.name, 'dist-tags', '--json'], { timeout: 90_000 }));
|
|
425
|
-
|
|
425
|
+
// A tracked non-latest tag (alpha, Kit's next) needs publish times to tell a newer pre-release from an abandoned
|
|
426
|
+
// one (developer-update-policy pickTargetTag); fetched only when that tag and latest name different versions. A
|
|
427
|
+
// failed lookup is {} — required but unknown — so the pre-release is skipped, never installed blind.
|
|
428
|
+
let published = null;
|
|
429
|
+
if (!before.localSource) {
|
|
430
|
+
let needed = false;
|
|
431
|
+
try { needed = needsPublishTimes(normalizeNpmDistTags(tags), wantedTag(before.name, config.channel)); } catch { needed = false; }
|
|
432
|
+
if (needed) {
|
|
433
|
+
try { published = JSON.parse(run(npm, ['view', before.name, 'time', '--json'], { timeout: 90_000 })); }
|
|
434
|
+
catch { published = {}; }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
receipt.steps.push(upgradePackage(before, tags, REVIEWED_INSTALL_SCRIPTS, { run, dryRun: mode === 'check', channel: config.channel, npm, preservePackages: config.preservePackages, published }));
|
|
426
438
|
persistReceipt();
|
|
427
439
|
}
|
|
428
440
|
receipt.plugins = await synchronizePlugins(run, mode === 'check', receipt.notes, { home, prefix, scope: config.scope, locate, unattended: !!env.RUVNET_NIGHTLY_IDENTITY });
|
|
@@ -32,7 +32,7 @@ export function runLearningRuflo(binary, args, db, { env = process.env, deadline
|
|
|
32
32
|
const cwd = rufloRunDir(db, { root: env.RUVNET_RUFLO_CWD_ROOT || path.join(env.HOME || env.USERPROFILE || os.homedir(), '.cache', 'ruvnet-brain', 'learning-ruflo') });
|
|
33
33
|
try {
|
|
34
34
|
const invocation = rufloInvocation(binary, args);
|
|
35
|
-
return spawnSync(invocation.executable, invocation.args, { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
35
|
+
return spawnSync(invocation.executable, invocation.args, { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' },
|
|
36
36
|
encoding: 'utf8', shell: false, timeout: Math.min(maxStepMs, remaining), killSignal: 'SIGKILL', maxBuffer: 65536, windowsHide: true });
|
|
37
37
|
} finally { fs.rmSync(cwd, { force: true, recursive: true }); }
|
|
38
38
|
}
|
|
@@ -245,7 +245,11 @@ export function rufloCwdFor(storePath, { root = rufloScratchRoot() } = {}) {
|
|
|
245
245
|
return ensurePrivateDir(path.join(root, projectKey));
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
// node_modules / package.json / package-lock.json: ruflo's startup self-update runs `npm install @claude-flow/cli@X
|
|
249
|
+
// --save-exact` in its cwd (ruflo v3 @claude-flow/cli dist/src/update/executor.js). Measured 2026-10-22: a 1.3 GB
|
|
250
|
+
// install landed here. Every Brain spawn now sets CLAUDE_FLOW_AUTO_UPDATE=false (update/rate-limiter.js reads it);
|
|
251
|
+
// these names are the backstop for a ruflo started without it.
|
|
252
|
+
export const RUFLO_CWD_ARTIFACTS = Object.freeze(['.swarm', '.claude', '.claude-flow', 'ruvector.db', 'node_modules', 'package.json', 'package-lock.json']);
|
|
249
253
|
const STALE_RUN_MS = 3_600_000;
|
|
250
254
|
|
|
251
255
|
/**
|
|
@@ -48,7 +48,7 @@ export function projectStore(projectDir, { deadlineAt = Date.now() + 3000 } = {}
|
|
|
48
48
|
export function rufloSpawnOptions(db, env = process.env) {
|
|
49
49
|
let cwd;
|
|
50
50
|
try { cwd = rufloRunDir(db, { root: rufloScratchRoot(env) }); } catch { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-ruflo-')); }
|
|
51
|
-
return { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0' }, cleanup: () => { try { fs.rmSync(cwd, { recursive: true, force: true }); } catch { /* swept later as a stale run- dir */ } } };
|
|
51
|
+
return { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' }, cleanup: () => { try { fs.rmSync(cwd, { recursive: true, force: true }); } catch { /* swept later as a stale run- dir */ } } };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** One `ruflo memory <args>` call, bounded by the caller's deadline. Resolves, never rejects. */
|
|
@@ -60,7 +60,7 @@ const TRANSCRIPT_TAIL_BYTES = 2 * 1024 * 1024;
|
|
|
60
60
|
const STEP_TIMEOUT_MS = 60_000;
|
|
61
61
|
export const RECEIPTS_MAX_BYTES = 256 * 1024;
|
|
62
62
|
export const TURN_STATUS_FRESH_MS /* ✓ only when the newest receipt is verified and younger */ = 24 * 3_600_000;
|
|
63
|
-
const RUFLO_ENV = { RUFLO_DAEMON_AUTOSTART: '0' };
|
|
63
|
+
const RUFLO_ENV = { RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' };
|
|
64
64
|
|
|
65
65
|
function textOf(content) {
|
|
66
66
|
if (typeof content === 'string') return content;
|
|
@@ -35,7 +35,7 @@ function ruflo(cwd, args) {
|
|
|
35
35
|
// live: ~/.npm-global/lib/node_modules/ruflo/node_modules/@claude-flow/cli/dist/src/services/
|
|
36
36
|
// daemon-autostart.js:85) — this doctor walks a whole fleet of project dirs and must not leave
|
|
37
37
|
// one daemon running per project as a side effect of asking each one a question.
|
|
38
|
-
const r = spawnSync(RUFLO, args, { cwd, encoding: 'utf8', timeout: 120000, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } });
|
|
38
|
+
const r = spawnSync(RUFLO, args, { cwd, encoding: 'utf8', timeout: 120000, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' } });
|
|
39
39
|
return { out: (r.stdout || '') + (r.stderr || ''), status: r.status };
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
//
|
|
23
23
|
// THE RETRY SLOT (2026-10-20). Measured on 38 nights: 24 failed, most on one transient blip, and nothing
|
|
24
|
-
// retried until the next calendar day. The dispatcher now also fires at RETRY_CRON (
|
|
24
|
+
// retried until the next calendar day. The dispatcher now also fires at RETRY_CRON (09:17 UTC since 2026-10-24, four hours
|
|
25
25
|
// after the night's first slot) and asks decideRetry(): re-dispatch ONLY when every corpus run of the
|
|
26
26
|
// night so far ended `failed` (or reached no outcome), none is still running, none ended published /
|
|
27
27
|
// no-change / superseded / canary-rejected (those are answers, not weather), and fewer than
|
|
@@ -37,9 +37,10 @@ import { pathToFileURL } from 'node:url';
|
|
|
37
37
|
|
|
38
38
|
export const MIN_PIPELINE_VERSION = '4.3.38'; // sync-version-ignore: a fixed pipeline floor, not the current version
|
|
39
39
|
/** The dispatcher's second schedule entry; corpus-nightly-dispatch.yml's `on.schedule` must carry exactly this line. */
|
|
40
|
-
export const RETRY_CRON = '17
|
|
41
|
-
/** How far back the retry slot looks for the night's runs: past the
|
|
42
|
-
|
|
40
|
+
export const RETRY_CRON = '17 9 * * *';
|
|
41
|
+
/** How far back the retry slot looks for the night's runs: past the 05:17 UTC first slot (with room for GitHub's late
|
|
42
|
+
* schedule starts), never into yesterday's slot or an evening manual dispatch. */
|
|
43
|
+
export const RETRY_WINDOW_MS = 8 * 3_600_000;
|
|
43
44
|
export const MAX_CORPUS_RUNS_PER_NIGHT = 2;
|
|
44
45
|
// `unknown` (every job green, no outcome) is retried only when the run itself did NOT conclude success: a green
|
|
45
46
|
// run that reached no outcome is a workflow defect the terminal-outcome job already fails loudly, not weather.
|
|
@@ -48,7 +48,7 @@ const HOME = os.homedir();
|
|
|
48
48
|
// install whose npm prefix is not the owner's (Homebrew, nvm, Volta, plain `npm -g`) was told its
|
|
49
49
|
// tool was missing while ruflo sat on their PATH. One resolver, shared — see ruflo-bin.mjs.
|
|
50
50
|
const RUFLO = resolveRuflo();
|
|
51
|
-
const RUFLO_ENV = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' };
|
|
51
|
+
const RUFLO_ENV = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' };
|
|
52
52
|
|
|
53
53
|
const argv = process.argv.slice(2);
|
|
54
54
|
const arg = (f, d) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
|
package/scripts/falsify.mjs
CHANGED
|
@@ -179,7 +179,7 @@ export const CHECKS = [
|
|
|
179
179
|
const rufloBin = resolveRuflo();
|
|
180
180
|
const r = rufloBin
|
|
181
181
|
? spawnSync(rufloBin, ['memory', 'search', '-q', 'nightly job supervision heartbeat receipts'], {
|
|
182
|
-
cwd: ROOT, encoding: 'utf8', timeout: 120_000, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
182
|
+
cwd: ROOT, encoding: 'utf8', timeout: 120_000, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' },
|
|
183
183
|
})
|
|
184
184
|
: { error: new Error(RUFLO_MISSING), stdout: '' };
|
|
185
185
|
const recalled = /Found\s+\d+\s+results/.test(r.stdout || '') && !/Found\s+0\s+results/.test(r.stdout || '');
|
|
@@ -54,7 +54,7 @@ const PROJECT = learningContext({ cwd: requestedProject || process.env.RUVNET_BR
|
|
|
54
54
|
* that one global binary is rather than assuming a path.
|
|
55
55
|
*/
|
|
56
56
|
const RUFLO = resolveRuflo();
|
|
57
|
-
const RUFLO_ENV = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' };
|
|
57
|
+
const RUFLO_ENV = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' };
|
|
58
58
|
|
|
59
59
|
const sqlite = (db, sql) => execFileSync('sqlite3', [db, sql], { encoding: 'utf8', timeout: 120_000 }).trim();
|
|
60
60
|
|
|
@@ -32,7 +32,7 @@ const MUTATING_SUBCOMMANDS = new Set([
|
|
|
32
32
|
// node_modules/@claude-flow/cli/dist/src/services/daemon-autostart.js:85). Applied here, once, so
|
|
33
33
|
// every caller of spawnRuflo() gets it regardless of whether it passed its own `env`.
|
|
34
34
|
function spawnRuflo(bin, args, options = {}) {
|
|
35
|
-
const opts = { ...options, env: { ...(options.env || process.env), RUFLO_DAEMON_AUTOSTART: '0' } };
|
|
35
|
+
const opts = { ...options, env: { ...(options.env || process.env), RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' } };
|
|
36
36
|
return /\.[cm]?js$/i.test(bin)
|
|
37
37
|
? spawnSync(process.execPath, [bin, ...args], opts)
|
|
38
38
|
: spawnSync(bin, args, opts);
|
|
@@ -120,7 +120,7 @@ export function executeProducedCommand(cmd, {
|
|
|
120
120
|
.find((value) => MUTATING_SUBCOMMANDS.has(value));
|
|
121
121
|
if (mutating) return reject(`refused mutating subcommand "${mutating}"`, { argv: ['ruflo', ...args] });
|
|
122
122
|
|
|
123
|
-
const env = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' };
|
|
123
|
+
const env = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' };
|
|
124
124
|
delete env.CLAUDE_FLOW_DB_PATH;
|
|
125
125
|
delete env.CLAUDE_FLOW_MEMORY_PATH;
|
|
126
126
|
const result = spawnRuflo(ruflo, args, {
|
|
@@ -151,7 +151,7 @@ export function verifyRufloFlag(bin = RUFLO_BIN) {
|
|
|
151
151
|
const result = spawnSync(bin, ['memory', 'search', '--help'], {
|
|
152
152
|
encoding: 'utf8',
|
|
153
153
|
timeout: 30_000,
|
|
154
|
-
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
154
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' },
|
|
155
155
|
});
|
|
156
156
|
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
|
157
157
|
if (result.status !== 0 && !output) return { ok: false, why: `help exited ${result.status} empty` };
|
|
@@ -32,6 +32,9 @@ const sh = (cmd, args, options = {}) => {
|
|
|
32
32
|
encoding: 'utf8',
|
|
33
33
|
timeout: 120_000,
|
|
34
34
|
...options,
|
|
35
|
+
// A ruflo started here would auto-start a daemon and run its in-cwd self-update (`npm install @claude-flow/cli`,
|
|
36
|
+
// 1.3 GB measured 2026-10-22) in the fixture project; a caller's own env still wins.
|
|
37
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false', ...(options.env || {}) },
|
|
35
38
|
});
|
|
36
39
|
};
|
|
37
40
|
const remove = (target) => {
|
|
@@ -96,7 +96,7 @@ function rufloJson(args) {
|
|
|
96
96
|
const tmp = path.join(SCRATCH_DIR, `call-${callCounter++}.json`);
|
|
97
97
|
const fd = fs.openSync(tmp, 'w');
|
|
98
98
|
try {
|
|
99
|
-
execFileSync(RUFLO, args, { stdio: ['ignore', fd, 'ignore'], timeout: CLI_TIMEOUT_MS, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } });
|
|
99
|
+
execFileSync(RUFLO, args, { stdio: ['ignore', fd, 'ignore'], timeout: CLI_TIMEOUT_MS, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' } });
|
|
100
100
|
} catch (e) {
|
|
101
101
|
if (e.signal || e.code === 'ETIMEDOUT') {
|
|
102
102
|
throw new Error(`ruflo ${args.join(' ')} did not finish within ${CLI_TIMEOUT_MS}ms (killed with ${e.signal || e.code}) — likely lock contention from a concurrent writer on this shared machine. Re-run.`);
|
|
@@ -150,10 +150,10 @@ fi
|
|
|
150
150
|
# Memory distillation (ADR-174) had gone stale 3 days — same silent-death pattern as everything
|
|
151
151
|
# else tonight. Independent of rebuild success/failure: mine raw memory_entries into structured
|
|
152
152
|
# episodes/reasoning_patterns on every nightly run. Best-effort, never blocks the real job.
|
|
153
|
-
RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory distill run --path "$RUVNET_PROJECT_MEMORY_DB" >> "$LOG" 2>&1 || true
|
|
153
|
+
RUFLO_DAEMON_AUTOSTART=0 CLAUDE_FLOW_AUTO_UPDATE=false ~/.npm-global/bin/ruflo memory distill run --path "$RUVNET_PROJECT_MEMORY_DB" >> "$LOG" 2>&1 || true
|
|
154
154
|
# Durability: was a one-off manual snapshot before tonight. Now recurring — WAL-safe, rotates
|
|
155
155
|
# automatically (keeps last 14), zero risk to the live DB (reads only).
|
|
156
|
-
RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory backup --db "$RUVNET_PROJECT_MEMORY_DB" --keep 14 >> "$LOG" 2>&1 || true
|
|
156
|
+
RUFLO_DAEMON_AUTOSTART=0 CLAUDE_FLOW_AUTO_UPDATE=false ~/.npm-global/bin/ruflo memory backup --db "$RUVNET_PROJECT_MEMORY_DB" --keep 14 >> "$LOG" 2>&1 || true
|
|
157
157
|
|
|
158
158
|
# LESSON BRIDGE (ADR-066, 2026-08-10) — carry newly-tagged machine-wide lessons into the store the
|
|
159
159
|
# hooks actually read. Without this the bridge is a command someone has to remember, and a learning
|
|
@@ -26,6 +26,7 @@ set -euo pipefail
|
|
|
26
26
|
# daemon-autostart.js:85). This script only checks/starts the proxy — it has no business leaving a
|
|
27
27
|
# swarm daemon running as a side effect.
|
|
28
28
|
export RUFLO_DAEMON_AUTOSTART=0
|
|
29
|
+
export CLAUDE_FLOW_AUTO_UPDATE=false
|
|
29
30
|
|
|
30
31
|
TOKEN_FILE="$HOME/.ruflo/proxy-token"
|
|
31
32
|
|
|
@@ -13,6 +13,7 @@ set -uo pipefail
|
|
|
13
13
|
# live: ~/.npm-global/lib/node_modules/ruflo/node_modules/@claude-flow/cli/dist/src/services/
|
|
14
14
|
# daemon-autostart.js:85). A teardown script has no business leaving one running.
|
|
15
15
|
export RUFLO_DAEMON_AUTOSTART=0
|
|
16
|
+
export CLAUDE_FLOW_AUTO_UPDATE=false
|
|
16
17
|
|
|
17
18
|
echo "Reverting the Meta LLM Proxy trial..."
|
|
18
19
|
echo
|
|
@@ -11,6 +11,7 @@ set -uo pipefail
|
|
|
11
11
|
# daemon-autostart.js:85). Installing/starting the proxy has no business leaving a swarm daemon
|
|
12
12
|
# running as a side effect.
|
|
13
13
|
export RUFLO_DAEMON_AUTOSTART=0
|
|
14
|
+
export CLAUDE_FLOW_AUTO_UPDATE=false
|
|
14
15
|
|
|
15
16
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
16
17
|
|
|
@@ -60,7 +60,7 @@ try {
|
|
|
60
60
|
// Every `ruflo` invocation auto-starts a project background daemon unless this is set
|
|
61
61
|
// (verified live: ~/.npm-global/lib/node_modules/ruflo/node_modules/@claude-flow/cli/dist/src/
|
|
62
62
|
// services/daemon-autostart.js:85) — a health-check probe must not leave one running.
|
|
63
|
-
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
63
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' },
|
|
64
64
|
});
|
|
65
65
|
doctorOk = /Summary:.*passed/.test(doctorOut) && !/\b\d+ failed/.test(doctorOut);
|
|
66
66
|
} catch (err) {
|
|
@@ -71,7 +71,7 @@ const ruflo = (args) => {
|
|
|
71
71
|
const invocation = rufloInvocation(RUFLO, args);
|
|
72
72
|
return execFileSync(invocation.executable, invocation.args, {
|
|
73
73
|
cwd: dir, encoding: 'utf8', timeout: 60000, shell: false,
|
|
74
|
-
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' },
|
|
74
|
+
env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' },
|
|
75
75
|
});
|
|
76
76
|
};
|
|
77
77
|
|
package/scripts/selfcheck.mjs
CHANGED
|
@@ -613,7 +613,7 @@ export function runSecurityScan({ cwd = process.cwd(), timeoutMs = 20000, env =
|
|
|
613
613
|
// Every `ruflo` invocation auto-starts a project background daemon unless this is set (verified
|
|
614
614
|
// live against the installed CLI: ~/.npm-global/lib/node_modules/ruflo/node_modules/@claude-flow/cli/dist/src/services/daemon-autostart.js:85).
|
|
615
615
|
// A read-only version probe + two scan subcommands have no business leaving a daemon running.
|
|
616
|
-
env = { ...env, RUFLO_DAEMON_AUTOSTART: '0' };
|
|
616
|
+
env = { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' };
|
|
617
617
|
const probe = spawnSync('ruflo', ['--version'], { encoding: 'utf8', timeout: 5000, env });
|
|
618
618
|
if (probe.error || probe.status !== 0) {
|
|
619
619
|
return { available: false, reason: 'ruflo not found on PATH — MCP surface not scanned (install: npm i -g ruflo)' };
|
|
@@ -240,7 +240,7 @@ const checks = [
|
|
|
240
240
|
run: () => { const s = readAbs(path.join(HOME, '.claude/settings.json')); return { ok: !/agentdb-autocapture/.test(s), detail: /agentdb-autocapture/.test(s) ? 'global agentdb-autocapture still registered alongside the plugin session-snapshot' : 'single writer' }; } },
|
|
241
241
|
|
|
242
242
|
{ id: 'E3', area: 'hooks', scope: 'machine', title: 'Lessons live in one store (no .swarm lesson-* rows outside the plugin lesson store)',
|
|
243
|
-
run: () => { const r = spawnSync('ruflo', ['memory', 'list', '--path', path.join(HOME, 'Code/ruvnet-brain/.swarm/memory.db'), '--limit', '500'], { encoding: 'utf8', timeout: 60000, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' } });
|
|
243
|
+
run: () => { const r = spawnSync('ruflo', ['memory', 'list', '--path', path.join(HOME, 'Code/ruvnet-brain/.swarm/memory.db'), '--limit', '500'], { encoding: 'utf8', timeout: 60000, env: { ...process.env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false' } });
|
|
244
244
|
const n = (r.stdout.match(/lesson-/g) || []).length; return { ok: n === 0, detail: `${n} lesson-* rows in project .swarm (plugin store: ~/.config/ruvnet-brain/lessons.json)` }; } },
|
|
245
245
|
{ id: 'E4', area: 'hooks', scope: 'machine', title: 'One session-start continuity restorer (global hook stands down when the plugin is enabled)',
|
|
246
246
|
run: () => { const s = readAbs(path.join(HOME, '.claude/hooks/agentdb-ensure.sh')); return { ok: s.includes('SINGLE CONTINUITY OWNER'), detail: 'agentdb-ensure.sh must stand down its project-state recall' }; } },
|
package/scripts/stack-sync.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import fs from 'node:fs';
|
|
|
37
37
|
import os from 'node:os';
|
|
38
38
|
import path from 'node:path';
|
|
39
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';
|
|
40
|
+
import { FAMILY, PLUGIN_MARKETPLACES, cmpVersion, pickTargetTag, normalizeNpmDistTags, normalizeNpmTimes, needsPublishTimes, wantedTag } from '../plugin/scripts/developer-update-policy.mjs';
|
|
41
41
|
export { FAMILY, PLUGIN_MARKETPLACES, cmpVersion, pickTargetTag, normalizeNpmDistTags } from '../plugin/scripts/developer-update-policy.mjs';
|
|
42
42
|
|
|
43
43
|
const HOME = os.homedir();
|
|
@@ -193,6 +193,13 @@ function registryTags(pkg) {
|
|
|
193
193
|
try { return normalizeNpmDistTags(JSON.parse(r.stdout)); } catch { return null; }
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// Publish times, so an abandoned pre-release tag is not reported as "behind" (same rule as the updater).
|
|
197
|
+
function registryTimes(pkg) {
|
|
198
|
+
const r = spawnSync('npm', ['view', pkg, 'time', '--json'], { encoding: 'utf8', timeout: 30000 });
|
|
199
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
200
|
+
try { return normalizeNpmTimes(JSON.parse(r.stdout)); } catch { return null; }
|
|
201
|
+
}
|
|
202
|
+
|
|
196
203
|
// A second copy of a stack package in the npx cache can only ever SHADOW the global one.
|
|
197
204
|
// It is never useful. This is how "two ruflos" happens.
|
|
198
205
|
export function findShadows(npxCache = NPX_CACHE, lib = GLOBAL_LIB) {
|
|
@@ -228,11 +235,12 @@ export function classify(pkgs, { channel = readDeveloperUpdateConfig().channel }
|
|
|
228
235
|
if (p.source === 'plugin') {
|
|
229
236
|
return { ...p, tag: 'plugin', target: null, state: p.installed ? 'INSTALLED_UNVERIFIED' : 'BROKEN', evidence: 'local install record only; marketplace revision not checked' };
|
|
230
237
|
}
|
|
231
|
-
const want = p.name
|
|
238
|
+
const want = wantedTag(p.name, channel);
|
|
232
239
|
const tags = registryTags(p.name);
|
|
233
240
|
// Newest of {policy tag, latest} — see pickTargetTag: "track @alpha" alone pinned the core behind
|
|
234
241
|
// @latest on 2026-07-18. A package with no alpha tag falls through to latest.
|
|
235
|
-
const
|
|
242
|
+
const published = needsPublishTimes(tags, want) ? (registryTimes(p.name) || {}) : null;
|
|
243
|
+
const { tag, target } = pickTargetTag(tags, want, 'latest', { published });
|
|
236
244
|
let state;
|
|
237
245
|
if (!p.installed) state = 'BROKEN';
|
|
238
246
|
else if (!target) state = 'UNRESOLVED';
|