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.
Files changed (43) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +120 -15
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/hooks/grok-hooks.json +1 -1
  7. package/plugin/hooks/hook-contracts.json +36 -18
  8. package/plugin/hooks/hooks.json +1 -1
  9. package/plugin/scripts/agentdb-recall-process.mjs +1 -1
  10. package/plugin/scripts/agentdb-recall-reader.mjs +1 -1
  11. package/plugin/scripts/brain-confirmation.mjs +8 -1
  12. package/plugin/scripts/brain-footprint.mjs +3 -1
  13. package/plugin/scripts/continuity-hook-policy.mjs +9 -9
  14. package/plugin/scripts/decision-record.mjs +1 -1
  15. package/plugin/scripts/degradation-watch.mjs +13 -1
  16. package/plugin/scripts/developer-update-policy.mjs +31 -5
  17. package/plugin/scripts/developer-update.mjs +17 -5
  18. package/plugin/scripts/grok-hook-adapter.mjs +7 -3
  19. package/plugin/scripts/hook-failures.mjs +76 -8
  20. package/plugin/scripts/learn-capture.mjs +74 -9
  21. package/plugin/scripts/learning-store.mjs +17 -7
  22. package/plugin/scripts/nightly-scheduler.mjs +67 -12
  23. package/plugin/scripts/ruflo-bin.mjs +5 -1
  24. package/plugin/scripts/ruflo-memory-hooks.mjs +1 -1
  25. package/plugin/scripts/turn-outcome-capture.mjs +18 -18
  26. package/scripts/agentdb-fleet-doctor.mjs +1 -1
  27. package/scripts/corpus-dispatch-decision.mjs +5 -4
  28. package/scripts/corpus-watchdog.mjs +28 -2
  29. package/scripts/distill-project.mjs +1 -1
  30. package/scripts/falsify.mjs +1 -1
  31. package/scripts/health-repair.mjs +1 -1
  32. package/scripts/learning-replay-execution.mjs +3 -3
  33. package/scripts/learning-replay-fixture.mjs +3 -0
  34. package/scripts/lesson-migrate-agentdb.mjs +1 -1
  35. package/scripts/nightly-wrapper.sh +2 -2
  36. package/scripts/proxy/claude-proxied.sh +1 -0
  37. package/scripts/proxy/proxy-revert.sh +1 -0
  38. package/scripts/proxy/proxy-up.sh +1 -0
  39. package/scripts/proxy/proxy-verify.mjs +1 -1
  40. package/scripts/record-lesson.mjs +1 -1
  41. package/scripts/selfcheck.mjs +1 -1
  42. package/scripts/single-source-check.mjs +1 -1
  43. 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
- function readTurnState(stateFile) {
243
- try { return JSON.parse(fs.readFileSync(stateFile, 'utf8')) || {}; } catch { return {}; }
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
- try {
250
- const rows = readTail(receipts, 128 * 1024).flatMap((line) => { try { return [JSON.parse(line)]; } catch { return []; } });
251
- const receipt = rows.findLast((r) => r.kind === 'store' && r.key === previous.key);
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 queue distillation (SessionEnd, PreCompact).
270
- * Returns a plain report; `queued` means a worker request, never proof the store committed.
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
- if (kind === 'store' && !verified && status === 0) verified = read({ ruflo, db, key, run, options }) === value;
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 && (r.error || status !== 0)) error = captureFailureReason(r, status);
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 (11:17 UTC, four hours
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 11 * * *';
41
- /** How far back the retry slot looks for the night's runs: past the 07:17 UTC first slot, never into yesterday. */
42
- export const RETRY_WINDOW_MS = 10 * 3_600_000;
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 dispatcher = (input?.dispatcherRuns || []).filter((d) => inWindow(d.createdAt)).sort((a, b) => ms(b.createdAt) - ms(a.createdAt))[0];
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 > 1 ? ` (attempt ${runs.length} of the night; ${runs.length - 1} retried)` : '';
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; };
@@ -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
 
@@ -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' }; } },
@@ -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 === '@pacphi/agentic-kit' ? 'next' : channel === 'alpha' && FAMILY.test(p.name) ? 'alpha' : DEFAULT_TAG;
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 { tag, target } = pickTargetTag(tags, want);
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';