ruvnet-brain 4.7.16 → 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 +120 -15
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/grok-hooks.json +1 -1
- package/plugin/hooks/hook-contracts.json +36 -18
- package/plugin/hooks/hooks.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/continuity-hook-policy.mjs +9 -9
- 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/grok-hook-adapter.mjs +7 -3
- package/plugin/scripts/hook-failures.mjs +76 -8
- package/plugin/scripts/learn-capture.mjs +74 -9
- package/plugin/scripts/learning-store.mjs +17 -7
- package/plugin/scripts/nightly-scheduler.mjs +67 -12
- package/plugin/scripts/ruflo-bin.mjs +5 -1
- package/plugin/scripts/ruflo-memory-hooks.mjs +1 -1
- package/plugin/scripts/turn-outcome-capture.mjs +18 -18
- package/scripts/agentdb-fleet-doctor.mjs +1 -1
- package/scripts/corpus-dispatch-decision.mjs +5 -4
- package/scripts/corpus-watchdog.mjs +28 -2
- 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
|
@@ -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;
|
|
@@ -239,20 +239,18 @@ export function launchDetached(steps, { receipts, brainHome, projectDir, env = p
|
|
|
239
239
|
return { launched: true, pid: child.pid };
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
const readTurnState = (stateFile) => { try { return JSON.parse(fs.readFileSync(stateFile, 'utf8')) || {}; } catch { return {}; } };
|
|
243
|
+
|
|
244
|
+
/** The newest store receipt for this exact key, while the receipt tail still holds it. */
|
|
245
|
+
const storeReceipt = (receipts, key) => { try { return readTail(receipts, 128 * 1024).flatMap((l) => { try { return [JSON.parse(l)]; } catch { return []; } }).findLast((r) => r.kind === 'store' && r.key === key) || null; } catch { return null; } };
|
|
246
|
+
const verifiedReceipt = (r) => r?.status === 0 && r.verified === true;
|
|
245
247
|
|
|
246
248
|
function sameTurnSeen(stateFile, sessionKey, fingerprint, receipts) {
|
|
247
249
|
const previous = readTurnState(stateFile)[sessionKey];
|
|
248
250
|
if (previous?.fingerprint !== fingerprint) return false;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (receipt) return receipt.status === 0 && receipt.verified === true;
|
|
253
|
-
} catch { /* queued worker has no receipt yet */ }
|
|
254
|
-
// An in-flight worker gets a bounded grace period; a killed/lost worker cannot consume the turn forever.
|
|
255
|
-
return Date.now() - previous.at < 2 * STEP_TIMEOUT_MS + 10_000;
|
|
251
|
+
const receipt = storeReceipt(receipts, previous.key);
|
|
252
|
+
// No receipt yet: an in-flight worker gets a bounded grace period; a killed/lost worker cannot consume the turn forever.
|
|
253
|
+
return receipt ? verifiedReceipt(receipt) : Date.now() - previous.at < 2 * STEP_TIMEOUT_MS + 10_000;
|
|
256
254
|
}
|
|
257
255
|
|
|
258
256
|
function markTurnQueued(stateFile, sessionKey, fingerprint, key) {
|
|
@@ -266,9 +264,8 @@ function markTurnQueued(stateFile, sessionKey, fingerprint, key) {
|
|
|
266
264
|
}
|
|
267
265
|
|
|
268
266
|
/**
|
|
269
|
-
* Capture this turn's outcome (Stop) and/or
|
|
270
|
-
*
|
|
271
|
-
* `skipped` / `distill.skipped` carry the reason whenever nothing is requested.
|
|
267
|
+
* Capture this turn's outcome (Stop) and/or start distillation (SessionEnd, PreCompact). Returns a plain report:
|
|
268
|
+
* `queued` means a request, never proof the store committed; `skipped` / `distill.skipped` carry the reason.
|
|
272
269
|
*/
|
|
273
270
|
export function captureTurnOutcome({
|
|
274
271
|
projectDir, event, payload = {}, host = 'claude',
|
|
@@ -322,10 +319,12 @@ export function captureTurnOutcome({
|
|
|
322
319
|
const fingerprint = crypto.createHash('sha256').update(JSON.stringify([turn.finalText, turn.files])).digest('hex');
|
|
323
320
|
const stateFile = path.join(brainHome, 'turn-capture', 'last-turn.json');
|
|
324
321
|
const identity = crypto.createHash('sha256').update(`${host}:${db}:${sessionKey}`).digest('hex');
|
|
322
|
+
const key = `turn-${project}-${digest(`${identity}:${fingerprint}`).slice(0, 40)}`;
|
|
325
323
|
if (sameTurnSeen(stateFile, identity, fingerprint, receipts)) report.skipped = 'same turn outcome already queued or verified';
|
|
324
|
+
// Proven at Stop already: SessionEnd/PreCompact must not re-send it (2026-10-19/20: each re-send was an "append rejected" row).
|
|
325
|
+
else if (verifiedReceipt(storeReceipt(receipts, key))) report.skipped = 'turn key already verified at an earlier boundary';
|
|
326
326
|
else {
|
|
327
327
|
const at = now();
|
|
328
|
-
const key = `turn-${project}-${digest(`${identity}:${fingerprint}`).slice(0, 40)}`;
|
|
329
328
|
dedupe = { stateFile, identity, fingerprint, key };
|
|
330
329
|
const value = buildTurnRecord({ turn, project, host, session: payload.session_id, at });
|
|
331
330
|
steps.push({ ...binding, kind: 'store', ruflo, args: ['memory', 'store', '--key', key, '--value', value, '--namespace', TURN_NAMESPACE,
|
|
@@ -444,13 +443,14 @@ export function runSteps({ steps = [], receipts, event = '' } = {}, { run: suppl
|
|
|
444
443
|
delete options.env.CLAUDE_FLOW_MEMORY_PATH;
|
|
445
444
|
const r = run(invocation.executable, invocation.args, options);
|
|
446
445
|
status = Number.isInteger(r.status) ? r.status : 1;
|
|
447
|
-
|
|
448
|
-
if (verified) status = 0;
|
|
446
|
+
const duplicate = kind === 'store' && status !== 0 && /already exists/i.test(captureFailureReason(r, status)); // append-only refusal
|
|
447
|
+
if (kind === 'store' && (status === 0 || duplicate)) { verified = read({ ruflo, db, key, run, options }) === value; if (verified) status = 0; } // identical bytes = success
|
|
449
448
|
if (kind === 'distill' && status === 0) {
|
|
450
449
|
const match = /(?:^|\n)\s*skipped:\s*([^\n]+)/i.exec(String(r.stdout || '').replace(/\x1b\[[0-9;]*m/g, ''));
|
|
451
450
|
if (match) skipped = redactText(match[1]).slice(0, 300);
|
|
452
451
|
}
|
|
453
|
-
if (!verified &&
|
|
452
|
+
if (!verified && duplicate) error = 'duplicate turn key holds different content';
|
|
453
|
+
else if (!verified && (r.error || status !== 0)) error = captureFailureReason(r, status);
|
|
454
454
|
else if (kind === 'store' && !verified) { status = 1; error = 'exact turn key/content readback failed'; }
|
|
455
455
|
} catch (e) { status = 1; error = redactText(e.message).slice(0, 300); }
|
|
456
456
|
if (error) error = maskExcludedPaths(error, privacyPatterns, projectDir);
|
|
@@ -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.
|
|
@@ -400,6 +400,31 @@ export function failedStage(run) {
|
|
|
400
400
|
return stage ? STAGE_NAMES[stage] || stage : `an unnamed job (conclusion ${run?.conclusion || 'unknown'})`;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
/** A night's scheduled dispatches (the 07:36Z run and its retry slot) sit within this span; a day earlier is last night. */
|
|
404
|
+
const NIGHT_SPAN_MS = 12 * HOUR;
|
|
405
|
+
/**
|
|
406
|
+
* How many protected-release corpus runs belong to THIS night. Measured 2026-10-22: a manual workflow_dispatch proving
|
|
407
|
+
* run at 10:36Z Oct 21 and a stood-down retry slot at 11:25Z Oct 21 made ONE scheduled night read "attempt 2; 1 retried".
|
|
408
|
+
* The night is anchored on the newest `schedule` dispatcher that dispatched a run, plus scheduled dispatchers within
|
|
409
|
+
* NIGHT_SPAN_MS before it; a run counts only from that start and never when its dispatcher was a manual
|
|
410
|
+
* workflow_dispatch. A stood-down retry slot created no run, so it never counts. `runs` is newest-first, in window.
|
|
411
|
+
*/
|
|
412
|
+
export function nightAttempts(runs, dispatchers) {
|
|
413
|
+
const dispatchKey = (d) => `corpus-${d.id}-${d.attempt ?? 1}`;
|
|
414
|
+
const runKey = (run) => CORPUS_RUN_TITLE.exec(String(run.title || ''))?.[1];
|
|
415
|
+
const byKey = new Map(dispatchers.map((d) => [dispatchKey(d), d]));
|
|
416
|
+
const manual = (run) => byKey.get(runKey(run))?.event === 'workflow_dispatch';
|
|
417
|
+
const [newest] = runs;
|
|
418
|
+
if (manual(newest)) return ' (1 attempt; manual dispatch)';
|
|
419
|
+
const scheduled = dispatchers.filter((d) => !d.event || d.event === 'schedule'); // an undated event is read as the schedule
|
|
420
|
+
const anchor = scheduled.find((d) => runs.some((run) => runKey(run) === dispatchKey(d)));
|
|
421
|
+
const nightStart = anchor
|
|
422
|
+
? Math.min(...scheduled.filter((d) => ms(d.createdAt) <= ms(anchor.createdAt) && ms(anchor.createdAt) - ms(d.createdAt) <= NIGHT_SPAN_MS).map((d) => ms(d.createdAt)))
|
|
423
|
+
: ms(newest.createdAt);
|
|
424
|
+
const count = runs.filter((run) => ms(run.createdAt) >= nightStart && !manual(run)).length || 1;
|
|
425
|
+
return count > 1 ? ` (attempt ${count} of the night; ${count - 1} retried)` : ' (1 attempt)';
|
|
426
|
+
}
|
|
427
|
+
|
|
403
428
|
/**
|
|
404
429
|
* THE ONE LINE the owner reads first: what the night did, result first, in Eastern time. `input` is
|
|
405
430
|
* gatherCorpusHealthInput's; the night is the newest corpus run of the last 24h and every retry of it.
|
|
@@ -410,9 +435,10 @@ export function summarizeNight(input, now) {
|
|
|
410
435
|
const inWindow = (iso) => { const t = ms(iso); return t !== null && t <= at && at - t <= TONIGHT_WINDOW_MS; };
|
|
411
436
|
const runs = (input?.corpusRuns || []).filter((run) => inWindow(run.createdAt))
|
|
412
437
|
.map((run) => ({ ...run, outcome: run.outcome ?? classifyCorpusRun(run) })).sort((a, b) => ms(b.createdAt) - ms(a.createdAt));
|
|
413
|
-
const
|
|
438
|
+
const dispatchers = (input?.dispatcherRuns || []).filter((d) => inWindow(d.createdAt)).sort((a, b) => ms(b.createdAt) - ms(a.createdAt));
|
|
439
|
+
const [dispatcher] = dispatchers;
|
|
414
440
|
const latest = input?.latestRelease;
|
|
415
|
-
const attempts = runs.length
|
|
441
|
+
const attempts = runs.length ? nightAttempts(runs, dispatchers) : '';
|
|
416
442
|
const keep = latest ? `customers keep ${latest.tag} (${hours(Math.max(0, at - (ms(latest.generation || latest.publishedAt) ?? at)))} old)` : 'no promoted generation is observed';
|
|
417
443
|
if (!runs.length) {
|
|
418
444
|
if (!dispatcher) return { outcome: 'no-run', line: `night: no corpus run in the last 24h (no dispatcher run either); ${keep}` };
|
|
@@ -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';
|