claude-flow 3.10.41 β†’ 3.10.43

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.
@@ -0,0 +1 @@
1
+ {"sessionId":"d0b433ab-a217-4dde-9ea4-b2c996ac456a","pid":35460,"acquiredAt":1781120150954}
package/README.md CHANGED
@@ -16,10 +16,6 @@
16
16
  [![Codex Plugin](https://img.shields.io/badge/Codex-Plugin-412991?style=for-the-badge&logoColor=white&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI%2BPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0yMi4yODIgOS44MjFhNS45ODUgNS45ODUgMCAwIDAtLjUxNi00LjkxIDYuMDQ2IDYuMDQ2IDAgMCAwLTYuNTEtMi45QTYuMDY1IDYuMDY1IDAgMCAwIDQuOTgxIDQuMThhNS45ODUgNS45ODUgMCAwIDAtMy45OTggMi45IDYuMDQ2IDYuMDQ2IDAgMCAwIC43NDMgNy4wOTcgNS45OCA1Ljk4IDAgMCAwIC41MSA0LjkxMSA2LjA1MSA2LjA1MSAwIDAgMCA2LjUxNSAyLjlBNS45ODUgNS45ODUgMCAwIDAgMTMuMjYgMjRhNi4wNTYgNi4wNTYgMCAwIDAgNS43NzItNC4yMDYgNS45OSA1Ljk5IDAgMCAwIDMuOTk4LTIuOSA2LjA1NiA2LjA1NiAwIDAgMC0uNzQ3LTcuMDczek0xMy4yNiAyMi40M2E0LjQ3NiA0LjQ3NiAwIDAgMS0yLjg3Ni0xLjA0bC4xNDItLjA4IDQuNzc4LTIuNzU4YS43OTUuNzk1IDAgMCAwIC4zOTMtLjY4MXYtNi43MzdsMi4wMiAxLjE2OGEuMDcxLjA3MSAwIDAgMSAuMDM4LjA1MnY1LjU4M2E0LjUwNCA0LjUwNCAwIDAgMS00LjQ5NSA0LjQ5NHpNMy42IDE4LjMwNGE0LjQ3IDQuNDcgMCAwIDEtLjUzNS0zLjAxNGwuMTQyLjA4NSA0Ljc4MyAyLjc1OWEuNzcxLjc3MSAwIDAgMCAuNzgxIDBsNS44NDMtMy4zNjl2Mi4zMzJhLjA4LjA4IDAgMCAxLS4wMzMuMDYyTDkuNzQgMTkuOTVhNC41IDQuNSAwIDAgMS02LjE0LTEuNjQ2ek0yLjM0IDcuODk2YTQuNDg1IDQuNDg1IDAgMCAxIDIuMzY2LTEuOTczVjExLjZhLjc2Ni43NjYgMCAwIDAgLjM4OC42NzdsNS44MTUgMy4zNTQtMi4wMiAxLjE2OGEuMDc2LjA3NiAwIDAgMS0uMDcyIDBsLTQuODMtMi43ODZBNC41MDQgNC41MDQgMCAwIDEgMi4zNCA3Ljg3MnptMTYuNTk3IDMuODU1LTUuODMzLTMuMzg3IDIuMDE2LTEuMTY1YS4wNzYuMDc2IDAgMCAxIC4wNzEgMGw0LjgzIDIuNzkxYTQuNDk0IDQuNDk0IDAgMCAxLS42NzYgOC4xMDR2LTUuNjc3YS43OS43OSAwIDAgMC0uNDA3LS42Njd6bTIuMDEtMy4wMjMtLjE0MS0uMDg1LTQuNzc0LTIuNzgyYS43NzYuNzc2IDAgMCAwLS43ODUgMEw5LjQwOSA5LjIzVjYuODk3YS4wNjYuMDY2IDAgMCAxIC4wMjgtLjA2Mmw0LjgzLTIuNzg3YTQuNDk5IDQuNDk5IDAgMCAxIDYuNjggNC42NnpNOC4zMDcgMTIuODYzbC0yLjAyLTEuMTY0YS4wOC4wOCAwIDAgMS0uMDM4LS4wNTdWNi4wNzRhNC40OTkgNC40OTkgMCAwIDEgNy4zNzYtMy40NTRsLS4xNDIuMDgtNC43NzggMi43NThhLjc5NS43OTUgMCAwIDAtLjM5My42ODJ6bTEuMDk3LTIuMzY2IDIuNjAyLTEuNSAyLjYwNyAxLjV2Mi45OTlsLTIuNTk3IDEuNS0yLjYwNy0xLjVaIi8%2BPC9zdmc%2B)](https://www.npmjs.com/package/@claude-flow/codex)
17
17
  [![πŸ•ΈοΈ RuVector Graph Ai](https://img.shields.io/badge/RuVector_Agentic-DB-06b6d4?style=for-the-badge&logoColor=white&logo=graphql)](https://github.com/ruvnet/ruvector)
18
18
 
19
- [![RuFlo Agentic Appliance](v3/docs/assets/RuFlo-agentic-appliance.png)](https://cognitum.one/appliance)
20
-
21
- [![ruFlo Summit β€” Budapest, June 2–3, 2026](v3/docs/assets/ruFlo-Summit.jpg)](https://github.com/ruvnet/ruflo/issues/1967)
22
-
23
19
  # Ruflo
24
20
 
25
21
  **Multi-agent AI harness for Claude Code and Codex**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.10.41",
3
+ "version": "3.10.43",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -16,10 +16,6 @@
16
16
  [![Codex Plugin](https://img.shields.io/badge/Codex-Plugin-412991?style=for-the-badge&logoColor=white&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI%2BPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0yMi4yODIgOS44MjFhNS45ODUgNS45ODUgMCAwIDAtLjUxNi00LjkxIDYuMDQ2IDYuMDQ2IDAgMCAwLTYuNTEtMi45QTYuMDY1IDYuMDY1IDAgMCAwIDQuOTgxIDQuMThhNS45ODUgNS45ODUgMCAwIDAtMy45OTggMi45IDYuMDQ2IDYuMDQ2IDAgMCAwIC43NDMgNy4wOTcgNS45OCA1Ljk4IDAgMCAwIC41MSA0LjkxMSA2LjA1MSA2LjA1MSAwIDAgMCA2LjUxNSAyLjlBNS45ODUgNS45ODUgMCAwIDAgMTMuMjYgMjRhNi4wNTYgNi4wNTYgMCAwIDAgNS43NzItNC4yMDYgNS45OSA1Ljk5IDAgMCAwIDMuOTk4LTIuOSA2LjA1NiA2LjA1NiAwIDAgMC0uNzQ3LTcuMDczek0xMy4yNiAyMi40M2E0LjQ3NiA0LjQ3NiAwIDAgMS0yLjg3Ni0xLjA0bC4xNDItLjA4IDQuNzc4LTIuNzU4YS43OTUuNzk1IDAgMCAwIC4zOTMtLjY4MXYtNi43MzdsMi4wMiAxLjE2OGEuMDcxLjA3MSAwIDAgMSAuMDM4LjA1MnY1LjU4M2E0LjUwNCA0LjUwNCAwIDAgMS00LjQ5NSA0LjQ5NHpNMy42IDE4LjMwNGE0LjQ3IDQuNDcgMCAwIDEtLjUzNS0zLjAxNGwuMTQyLjA4NSA0Ljc4MyAyLjc1OWEuNzcxLjc3MSAwIDAgMCAuNzgxIDBsNS44NDMtMy4zNjl2Mi4zMzJhLjA4LjA4IDAgMCAxLS4wMzMuMDYyTDkuNzQgMTkuOTVhNC41IDQuNSAwIDAgMS02LjE0LTEuNjQ2ek0yLjM0IDcuODk2YTQuNDg1IDQuNDg1IDAgMCAxIDIuMzY2LTEuOTczVjExLjZhLjc2Ni43NjYgMCAwIDAgLjM4OC42NzdsNS44MTUgMy4zNTQtMi4wMiAxLjE2OGEuMDc2LjA3NiAwIDAgMS0uMDcyIDBsLTQuODMtMi43ODZBNC41MDQgNC41MDQgMCAwIDEgMi4zNCA3Ljg3MnptMTYuNTk3IDMuODU1LTUuODMzLTMuMzg3IDIuMDE2LTEuMTY1YS4wNzYuMDc2IDAgMCAxIC4wNzEgMGw0LjgzIDIuNzkxYTQuNDk0IDQuNDk0IDAgMCAxLS42NzYgOC4xMDR2LTUuNjc3YS43OS43OSAwIDAgMC0uNDA3LS42Njd6bTIuMDEtMy4wMjMtLjE0MS0uMDg1LTQuNzc0LTIuNzgyYS43NzYuNzc2IDAgMCAwLS43ODUgMEw5LjQwOSA5LjIzVjYuODk3YS4wNjYuMDY2IDAgMCAxIC4wMjgtLjA2Mmw0LjgzLTIuNzg3YTQuNDk5IDQuNDk5IDAgMCAxIDYuNjggNC42NnpNOC4zMDcgMTIuODYzbC0yLjAyLTEuMTY0YS4wOC4wOCAwIDAgMS0uMDM4LS4wNTdWNi4wNzRhNC40OTkgNC40OTkgMCAwIDEgNy4zNzYtMy40NTRsLS4xNDIuMDgtNC43NzggMi43NThhLjc5NS43OTUgMCAwIDAtLjM5My42ODJ6bTEuMDk3LTIuMzY2IDIuNjAyLTEuNSAyLjYwNyAxLjV2Mi45OTlsLTIuNTk3IDEuNS0yLjYwNy0xLjVaIi8%2BPC9zdmc%2B)](https://www.npmjs.com/package/@claude-flow/codex)
17
17
  [![πŸ•ΈοΈ RuVector Graph Ai](https://img.shields.io/badge/RuVector_Agentic-DB-06b6d4?style=for-the-badge&logoColor=white&logo=graphql)](https://github.com/ruvnet/ruvector)
18
18
 
19
- [![RuFlo Agentic Appliance](v3/docs/assets/RuFlo-agentic-appliance.png)](https://cognitum.one/appliance)
20
-
21
- [![ruFlo Summit β€” Budapest, June 2–3, 2026](v3/docs/assets/ruFlo-Summit.jpg)](https://github.com/ruvnet/ruflo/issues/1967)
22
-
23
19
  # Ruflo
24
20
 
25
21
  **Multi-agent AI harness for Claude Code and Codex**
@@ -23,6 +23,14 @@ export declare function resolveWorkspaceFlag(raw: unknown): string | null;
23
23
  * via the PID file.
24
24
  */
25
25
  export declare function daemonCommandLineBelongsToWorkspace(commandLine: string, workspaceRoot: string): boolean;
26
+ /**
27
+ * #2356: extract the workspace root from a daemon process command line for the
28
+ * global `daemon status --all` view. The launcher always appends
29
+ * `--workspace <root>` as the FINAL argv entry (see startBackgroundDaemon), so
30
+ * we capture everything after it to end-of-line and strip trailing quotes.
31
+ * Returns null for pre-#1914 daemons that never stamped a workspace.
32
+ */
33
+ export declare function extractWorkspaceFromDaemonLine(commandLine: string): string | null;
26
34
  export declare const daemonCommand: Command;
27
35
  export default daemonCommand;
28
36
  //# sourceMappingURL=daemon.d.ts.map
@@ -21,6 +21,9 @@ const startCommand = {
21
21
  { name: 'sandbox', type: 'string', description: 'Default sandbox mode for headless workers', choices: ['strict', 'permissive', 'disabled'] },
22
22
  { name: 'max-cpu-load', type: 'string', description: 'Override maxCpuLoad resource threshold (e.g. 4.0)' },
23
23
  { name: 'min-free-memory', type: 'string', description: 'Override minFreeMemoryPercent resource threshold (e.g. 15)' },
24
+ // #2356: self-terminating lifecycle. Caps how long a forgotten daemon can
25
+ // keep dispatching headless worker sweeps. Default 12h (or RUFLO_DAEMON_TTL_SECS); 0 = run until stopped.
26
+ { name: 'ttl', type: 'string', description: 'Max daemon age in seconds before graceful self-shutdown (0 = run until stopped; default 43200 = 12h)' },
24
27
  // #1914: workspace root for this daemon. Set automatically when the
25
28
  // background launcher forks the foreground child so the daemon process
26
29
  // carries its workspace path in argv β€” `killStaleDaemons` then only
@@ -71,6 +74,18 @@ const startCommand = {
71
74
  config.resourceThresholds = thresholds;
72
75
  }
73
76
  }
77
+ // #2356: parse --ttl (seconds β†’ ms). Integer-only so 0 (disable) is valid;
78
+ // INT_RE forbids the decimals NUMERIC_RE allows, since a TTL is whole seconds.
79
+ const rawTtl = ctx.flags.ttl;
80
+ const INT_RE = /^\d+$/;
81
+ if (rawTtl !== undefined) {
82
+ if (INT_RE.test(rawTtl)) {
83
+ config.ttlMs = parseInt(rawTtl, 10) * 1000;
84
+ }
85
+ else if (!quiet) {
86
+ output.printWarning(`Ignoring invalid --ttl value: ${sanitize(rawTtl)}`);
87
+ }
88
+ }
74
89
  // Check if background daemon already running (skip if we ARE the daemon process)
75
90
  if (!isDaemonProcess) {
76
91
  const bgPid = getBackgroundDaemonPid(projectRoot);
@@ -95,6 +110,7 @@ const startCommand = {
95
110
  workers: ctx.flags.workers,
96
111
  headless: ctx.flags.headless,
97
112
  sandbox: ctx.flags.sandbox,
113
+ ttl: rawTtl,
98
114
  });
99
115
  }
100
116
  // Foreground mode: run in current process (blocks terminal)
@@ -134,6 +150,9 @@ const startCommand = {
134
150
  output.printBox([
135
151
  `PID: ${status.pid}`,
136
152
  `Started: ${status.startedAt?.toISOString()}`,
153
+ status.config.ttlMs > 0
154
+ ? `TTL: ${Math.round(status.config.ttlMs / 3600000)}h (self-shutdown)`
155
+ : `TTL: off (runs until stopped)`,
137
156
  `Workers: ${status.config.workers.filter(w => w.enabled).length} enabled`,
138
157
  `Max Concurrent: ${status.config.maxConcurrent}`,
139
158
  `Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
@@ -243,8 +262,22 @@ export function resolveWorkspaceFlag(raw) {
243
262
  export function daemonCommandLineBelongsToWorkspace(commandLine, workspaceRoot) {
244
263
  return commandLine.replace(/[\s"']+$/u, '').endsWith(`--workspace ${workspaceRoot}`);
245
264
  }
265
+ /**
266
+ * #2356: extract the workspace root from a daemon process command line for the
267
+ * global `daemon status --all` view. The launcher always appends
268
+ * `--workspace <root>` as the FINAL argv entry (see startBackgroundDaemon), so
269
+ * we capture everything after it to end-of-line and strip trailing quotes.
270
+ * Returns null for pre-#1914 daemons that never stamped a workspace.
271
+ */
272
+ export function extractWorkspaceFromDaemonLine(commandLine) {
273
+ const m = commandLine.match(/--workspace\s+(.+?)\s*$/u);
274
+ if (!m)
275
+ return null;
276
+ const ws = m[1].replace(/["']+$/u, '').trim();
277
+ return ws.length > 0 ? ws : null;
278
+ }
246
279
  async function startBackgroundDaemon(projectRoot, quiet, forwarded = {}) {
247
- const { maxCpuLoad, minFreeMemory, workers, headless, sandbox } = forwarded;
280
+ const { maxCpuLoad, minFreeMemory, workers, headless, sandbox, ttl } = forwarded;
248
281
  // Validate and resolve project root
249
282
  const resolvedRoot = resolve(projectRoot);
250
283
  validatePath(resolvedRoot, 'Project root');
@@ -312,6 +345,11 @@ async function startBackgroundDaemon(projectRoot, quiet, forwarded = {}) {
312
345
  if (minFreeMemory && SPAWN_NUMERIC_RE.test(minFreeMemory)) {
313
346
  forkArgs.push('--min-free-memory', minFreeMemory);
314
347
  }
348
+ // #2356: forward the TTL so the background daemon enforces it too. Integer
349
+ // seconds only (incl. 0 to disable) β€” reject anything else before it hits argv.
350
+ if (typeof ttl === 'string' && /^\d+$/.test(ttl)) {
351
+ forkArgs.push('--ttl', ttl);
352
+ }
315
353
  // #1968: forward worker-selection / sandbox flags. The previous launcher
316
354
  // dropped these, so `daemon start --workers map` ran with the default
317
355
  // five-worker set instead of just `map`. Validate each before passing
@@ -599,6 +637,116 @@ function isProcessRunning(pid) {
599
637
  return false;
600
638
  }
601
639
  }
640
+ /**
641
+ * #2356: enumerate every running ruflo daemon across ALL workspaces. Reuses
642
+ * the same `ps`/`tasklist` scan as killStaleDaemons but, instead of killing,
643
+ * returns each live daemon's PID + workspace so `daemon status --all` can
644
+ * surface daemons leaked in other projects. Best-effort: any tooling failure
645
+ * yields an empty list (matching the kill-stale paths).
646
+ */
647
+ async function scanRunningDaemons() {
648
+ const isWin = process.platform === 'win32';
649
+ try {
650
+ const { execFileSync } = await import('child_process');
651
+ const out = isWin
652
+ ? execFileSync('tasklist', ['/v', '/fo', 'csv', '/nh'], { encoding: 'utf-8', timeout: 5000 })
653
+ : execFileSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 5000 });
654
+ const lines = out.split(/\r?\n/);
655
+ const found = [];
656
+ for (const line of lines) {
657
+ if (!line.includes('daemon start --foreground'))
658
+ continue;
659
+ if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
660
+ continue;
661
+ let pid;
662
+ let cmd;
663
+ if (isWin) {
664
+ // tasklist /fo csv: quoted fields; PID is field[1], Window Title is last.
665
+ const fields = line.split(/","/).map(f => f.replace(/^"|"$/g, ''));
666
+ pid = parseInt(fields[1] ?? '', 10);
667
+ cmd = fields[fields.length - 1] ?? line;
668
+ }
669
+ else {
670
+ pid = parseInt(line.trim().split(/\s+/)[0], 10);
671
+ cmd = line;
672
+ }
673
+ if (Number.isNaN(pid) || !isProcessRunning(pid))
674
+ continue;
675
+ found.push({ pid, workspace: extractWorkspaceFromDaemonLine(cmd) });
676
+ }
677
+ return found;
678
+ }
679
+ catch {
680
+ return [];
681
+ }
682
+ }
683
+ /**
684
+ * #2356: render the global `daemon status --all` view. For each running daemon
685
+ * it reads that workspace's daemon-state.json to show age + configured TTL,
686
+ * and flags any daemon that has outlived its TTL (or 12h when TTL is unknown)
687
+ * as stale β€” the visibility that was missing when leaked daemons ran for days.
688
+ */
689
+ async function renderAllDaemonsStatus() {
690
+ const daemons = await scanRunningDaemons();
691
+ output.writeln();
692
+ if (daemons.length === 0) {
693
+ output.printBox('No ruflo daemons are running in any workspace.', 'RuFlo Daemons (all workspaces)');
694
+ return { success: true, data: { daemons: [] } };
695
+ }
696
+ const now = Date.now();
697
+ const TWELVE_HOURS_MS = 12 * 60 * 60 * 1000;
698
+ let staleCount = 0;
699
+ const rows = daemons.map(d => {
700
+ let startedAt;
701
+ let ttlMs;
702
+ if (d.workspace) {
703
+ try {
704
+ const statePath = join(d.workspace, '.claude-flow', 'daemon-state.json');
705
+ if (fs.existsSync(statePath)) {
706
+ const st = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
707
+ if (st?.startedAt)
708
+ startedAt = new Date(st.startedAt);
709
+ if (typeof st?.config?.ttlMs === 'number')
710
+ ttlMs = st.config.ttlMs;
711
+ }
712
+ }
713
+ catch { /* unreadable/partial state β€” show what we have */ }
714
+ }
715
+ const ageMs = startedAt ? now - startedAt.getTime() : undefined;
716
+ const overTtl = ttlMs !== undefined && ttlMs > 0 && ageMs !== undefined && ageMs > ttlMs;
717
+ const overTwelveH = ageMs !== undefined && ageMs > TWELVE_HOURS_MS;
718
+ const isStale = overTtl || overTwelveH;
719
+ if (isStale)
720
+ staleCount++;
721
+ const ageText = ageMs !== undefined ? formatTimeAgo(startedAt).replace(' ago', '') : '?';
722
+ const ttlText = ttlMs !== undefined
723
+ ? (ttlMs > 0 ? `${Math.round(ttlMs / 3600000)}h` : 'off')
724
+ : '?';
725
+ return {
726
+ pid: isStale ? output.warning(String(d.pid)) : String(d.pid),
727
+ workspace: d.workspace ?? output.dim('(unknown)'),
728
+ age: isStale ? output.warning(ageText) : ageText,
729
+ ttl: ttlText === 'off' ? output.dim('off') : ttlText,
730
+ };
731
+ });
732
+ output.printTable({
733
+ columns: [
734
+ { key: 'pid', header: 'PID', width: 8 },
735
+ { key: 'age', header: 'Age', width: 8 },
736
+ { key: 'ttl', header: 'TTL', width: 6 },
737
+ { key: 'workspace', header: 'Workspace', width: 50 },
738
+ ],
739
+ data: rows,
740
+ });
741
+ output.writeln();
742
+ if (staleCount > 0) {
743
+ output.printWarning(`${staleCount} daemon(s) have outlived their TTL (or have run >12h). Stop one with: cd <workspace> && ruflo daemon stop`);
744
+ }
745
+ else {
746
+ output.printInfo(`${daemons.length} daemon(s) running, all within their TTL.`);
747
+ }
748
+ return { success: true, data: { daemons: rows.length } };
749
+ }
602
750
  // Status subcommand
603
751
  const statusCommand = {
604
752
  name: 'status',
@@ -606,15 +754,24 @@ const statusCommand = {
606
754
  options: [
607
755
  { name: 'verbose', short: 'v', type: 'boolean', description: 'Show detailed worker statistics' },
608
756
  { name: 'show-modes', type: 'boolean', description: 'Show worker execution modes (local/headless) and sandbox settings' },
757
+ // #2356: the default status reads only the CURRENT workspace, so a daemon
758
+ // leaked in another project is invisible. --all scans every running ruflo
759
+ // daemon across all workspaces (the global view that surfaces leaks).
760
+ { name: 'all', short: 'a', type: 'boolean', description: 'List ruflo daemons across ALL workspaces (global view β€” surfaces leaked daemons)' },
609
761
  ],
610
762
  examples: [
611
763
  { command: 'claude-flow daemon status', description: 'Show daemon status' },
612
764
  { command: 'claude-flow daemon status -v', description: 'Show detailed status' },
613
765
  { command: 'claude-flow daemon status --show-modes', description: 'Show worker execution modes' },
766
+ { command: 'claude-flow daemon status --all', description: 'List daemons across all workspaces' },
614
767
  ],
615
768
  action: async (ctx) => {
616
769
  const verbose = ctx.flags.verbose;
617
770
  const showModes = ctx.flags['show-modes'];
771
+ // #2356: global view across every workspace, not just cwd.
772
+ if (ctx.flags.all) {
773
+ return renderAllDaemonsStatus();
774
+ }
618
775
  const projectRoot = process.cwd();
619
776
  try {
620
777
  const daemon = getDaemon(projectRoot);
@@ -633,6 +790,9 @@ const statusCommand = {
633
790
  `Status: ${statusIcon} ${statusText}${mode}`,
634
791
  `PID: ${displayPid}`,
635
792
  status.startedAt ? `Started: ${status.startedAt.toISOString()}` : '',
793
+ status.config.ttlMs > 0
794
+ ? `TTL: ${Math.round(status.config.ttlMs / 3600000)}h (self-shutdown)`
795
+ : `TTL: ${output.dim('off (runs until stopped)')}`,
636
796
  `Workers Enabled: ${status.config.workers.filter(w => w.enabled).length}`,
637
797
  `Max Concurrent: ${status.config.maxConcurrent}`,
638
798
  `Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
@@ -399,9 +399,20 @@ const postEditCommand = {
399
399
  metrics,
400
400
  timestamp: Date.now(),
401
401
  });
402
+ // #2352: the MCP handler returns `{success: false, error: "..."}` on
403
+ // validation failure (e.g. unsupported path shape) without throwing.
404
+ // Surface that explicitly instead of always printing the success line β€”
405
+ // Windows users were seeing `[OK]` while nothing reached the learning
406
+ // pipeline because absolute paths were rejected upstream.
407
+ const mcpFailed = result && result.success === false;
408
+ const mcpError = result?.error;
402
409
  if (ctx.flags.format === 'json') {
403
410
  output.printJson(result);
404
- return { success: true, data: result };
411
+ return { success: !mcpFailed, exitCode: mcpFailed ? 1 : 0, data: result };
412
+ }
413
+ if (mcpFailed) {
414
+ output.printError(`Post-edit hook failed: ${mcpError || 'unknown error'}`);
415
+ return { success: false, exitCode: 1 };
405
416
  }
406
417
  output.writeln();
407
418
  output.printSuccess(`Outcome recorded for ${filePath}`);
@@ -190,6 +190,7 @@ const initAction = async (ctx) => {
190
190
  // set still got `~/.claude/CLAUDE.md` modified. Read the real key.
191
191
  const noGlobal = ctx.flags['no-global'] === true || ctx.flags['global'] === false;
192
192
  const allAgents = ctx.flags['all-agents'];
193
+ const cloudMcp = ctx.flags['cloud-mcp'];
193
194
  const codexMode = ctx.flags.codex;
194
195
  const dualMode = ctx.flags.dual;
195
196
  const cwd = ctx.cwd;
@@ -230,6 +231,12 @@ const initAction = async (ctx) => {
230
231
  }
231
232
  else if (full) {
232
233
  options = { ...FULL_INIT_OPTIONS, targetDir: cwd, force };
234
+ // #2356: keep auth-gated cloud MCP servers opt-in even under --full. They
235
+ // require a login, get committed into .mcp.json, and add per-session MCP
236
+ // tool-definition token cost. --cloud-mcp restores the all-three behavior.
237
+ if (!cloudMcp) {
238
+ options.mcp = { ...options.mcp, ruvSwarm: false, flowNexus: false };
239
+ }
233
240
  }
234
241
  else {
235
242
  options = { ...DEFAULT_INIT_OPTIONS, targetDir: cwd, force };
@@ -773,7 +780,13 @@ const hooksCommand = {
773
780
  skills: false,
774
781
  commands: false,
775
782
  agents: false,
776
- helpers: false,
783
+ // #2350: helpers MUST ship with the hooks subcommand. The hook entries
784
+ // in settings.json point at `.claude/helpers/hook-handler.cjs`; if
785
+ // that file doesn't exist, settings-generator (#1744 fix) drops the
786
+ // hooks block entirely β€” so the one subcommand whose stated purpose
787
+ // is "Initialize only hooks configuration" produced settings.json
788
+ // with no `hooks` key while reporting "N hooks enabled".
789
+ helpers: true,
777
790
  statusline: false,
778
791
  mcp: false,
779
792
  runtime: false,
@@ -964,6 +977,16 @@ export const initCommand = {
964
977
  type: 'boolean',
965
978
  default: false,
966
979
  },
980
+ {
981
+ // #2356: under --full, the auth-gated cloud MCP servers (ruv-swarm,
982
+ // flow-nexus) get written into a committed .mcp.json and add MCP
983
+ // tool-definition token cost every session. Keep them opt-in even with
984
+ // --full; pass --cloud-mcp to register them.
985
+ name: 'cloud-mcp',
986
+ description: 'Register auth-gated cloud MCP servers (ruv-swarm, flow-nexus) in .mcp.json (only relevant with --full)',
987
+ type: 'boolean',
988
+ default: false,
989
+ },
967
990
  {
968
991
  name: 'skip-claude',
969
992
  description: 'Skip .claude/ directory creation (runtime only)',
@@ -411,11 +411,21 @@ const statusCommand = {
411
411
  details: `${stats.patternsLearned} patterns stored`,
412
412
  },
413
413
  {
414
+ // #2356: distinguish "loaded in this process" from "installed but
415
+ // not yet loaded" from "not installed". Previously `neural status`
416
+ // always printed "Not loaded" because it never warms the lazy
417
+ // singleton β€” a false negative even when @ruvector/core is present.
414
418
  component: 'HNSW Index',
415
- status: hnswStatus.available ? output.success('Ready') : output.dim('Not loaded'),
416
- details: hnswStatus.available
419
+ status: hnswStatus.initialized
420
+ ? output.success('Ready')
421
+ : hnswStatus.available
422
+ ? output.info('Available')
423
+ : output.dim('Not installed'),
424
+ details: hnswStatus.initialized
417
425
  ? `${hnswStatus.entryCount} vectors, ${hnswStatus.dimensions}-dim`
418
- : '@ruvector/core not available',
426
+ : hnswStatus.available
427
+ ? '@ruvector/core installed (loads on first vector search)'
428
+ : '@ruvector/core not available',
419
429
  },
420
430
  {
421
431
  component: 'Embedding Model',
@@ -172,11 +172,15 @@ function setupAndBoundary() {
172
172
  return `## Setup
173
173
 
174
174
  \`\`\`bash
175
- claude mcp add claude-flow -- npx -y @claude-flow/cli@latest
176
- npx @claude-flow/cli@latest daemon start
177
- npx @claude-flow/cli@latest doctor --fix
175
+ claude mcp add claude-flow -- npx -y ruflo@latest mcp start
176
+ npx ruflo@latest doctor --fix
178
177
  \`\`\`
179
178
 
179
+ > The background \`daemon\` is optional. It runs interval workers that each spawn
180
+ > a headless \`claude\` session, so it consumes tokens continuously. Start it only
181
+ > if you want those sweeps: \`npx ruflo@latest daemon start\` (self-stops after 12h
182
+ > by default; \`--ttl 0\` to disable, \`daemon status --all\` to audit running daemons).
183
+
180
184
  **Agent tool** handles execution (agents, files, code, git). **MCP tools** handle coordination (swarm, memory, hooks). **CLI** is the same via Bash.`;
181
185
  }
182
186
  function buildAndTest() {
@@ -22,6 +22,7 @@ export interface AgentRecord {
22
22
  lastResult?: Record<string, unknown>;
23
23
  }
24
24
  export declare const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6";
25
+ export declare function modelRejectsSamplingParams(model: string): boolean;
25
26
  export interface AnthropicCallInput {
26
27
  prompt: string;
27
28
  systemPrompt?: string;
@@ -46,6 +46,15 @@ const MODEL_MAP = {
46
46
  'opus-4.7': 'claude-opus-4-7',
47
47
  inherit: DEFAULT_ANTHROPIC_MODEL,
48
48
  };
49
+ // #2357 β€” the adaptive-thinking family (Fable 5, Opus 4.8, Opus 4.7) removed
50
+ // the sampling parameters (temperature/top_p/top_k); the Anthropic API
51
+ // returns 400 "Extra inputs are not permitted" when any is present.
52
+ // Prefix-match so dated snapshots (e.g. claude-opus-4-8-YYYYMMDD) are
53
+ // covered. Applies only to the direct Anthropic path β€” the Ollama/OpenRouter
54
+ // OpenAI-compat paths accept temperature and are unchanged.
55
+ export function modelRejectsSamplingParams(model) {
56
+ return /^claude-(fable-5|opus-4-8|opus-4-7)/.test(model);
57
+ }
49
58
  /**
50
59
  * Generic Anthropic Messages API call. No agent registry coupling β€” used
51
60
  * by agent_execute (with the agent's configured model) and by the WASM
@@ -76,7 +85,11 @@ export async function callAnthropicMessages(input) {
76
85
  apiKey: openrouterKey,
77
86
  baseUrl: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api',
78
87
  providerLabel: 'openrouter',
79
- defaultModel: process.env.OPENROUTER_DEFAULT_MODEL || 'anthropic/claude-3.5-sonnet',
88
+ // #2357 Finding C: anthropic/claude-3.5-sonnet was retired Oct 2025.
89
+ // Default to the same canonical family the rest of the resolver uses
90
+ // (MODEL_MAP). `OPENROUTER_DEFAULT_MODEL` still wins for callers who
91
+ // want to pin a specific OpenRouter slug.
92
+ defaultModel: process.env.OPENROUTER_DEFAULT_MODEL || 'anthropic/claude-sonnet-4-6',
80
93
  });
81
94
  }
82
95
  if (useOllama && ollamaKey) {
@@ -103,7 +116,13 @@ export async function callAnthropicMessages(input) {
103
116
  body: JSON.stringify({
104
117
  model,
105
118
  max_tokens: input.maxTokens || 1024,
106
- temperature: typeof input.temperature === 'number' ? input.temperature : 0.7,
119
+ // #2357 β€” omit temperature for models that reject sampling params
120
+ // (Fable 5 / Opus 4.8 / Opus 4.7 β†’ 400 "Extra inputs are not
121
+ // permitted"); keep the 0.7 default unchanged for models that still
122
+ // accept it (sonnet / haiku / opus ≀4.6).
123
+ ...(modelRejectsSamplingParams(model)
124
+ ? {}
125
+ : { temperature: typeof input.temperature === 'number' ? input.temperature : 0.7 }),
107
126
  // #8 prompt caching (hermes-agent pattern): mark the (often large,
108
127
  // stable) system prompt as an ephemeral cache breakpoint so repeated
109
128
  // agent_execute calls with the same system prompt hit Anthropic's
@@ -299,13 +318,15 @@ async function callOpenAICompat(input) {
299
318
  function resolveOpenAICompatModel(input, fallback) {
300
319
  if (!input)
301
320
  return fallback;
302
- // Logical Claude names β†’ OpenRouter Anthropic-vendored names
321
+ // Logical Claude names β†’ OpenRouter Anthropic-vendored names.
322
+ // #2357 Finding C: the 3.5 / 3-opus slugs were retired Oct 2025; align with
323
+ // MODEL_MAP (claude-haiku-4-5 / claude-sonnet-4-6 / claude-opus-4-8).
303
324
  if (input === 'haiku')
304
- return 'anthropic/claude-3.5-haiku';
325
+ return 'anthropic/claude-haiku-4-5';
305
326
  if (input === 'sonnet' || input === 'inherit')
306
- return 'anthropic/claude-3.5-sonnet';
327
+ return 'anthropic/claude-sonnet-4-6';
307
328
  if (input === 'opus')
308
- return 'anthropic/claude-3-opus';
329
+ return 'anthropic/claude-opus-4-8';
309
330
  return input;
310
331
  }
311
332
  function resolveOllamaModel(input) {
@@ -2665,7 +2665,81 @@ export const hooksTrajectoryEnd = {
2665
2665
  }
2666
2666
  catch { /* intelligence module not loadable β€” keep sona-only behaviour */ }
2667
2667
  }
2668
+ // #2351: when an agent calls trajectory-end with no recorded steps but a
2669
+ // non-empty `feedback` string, the feedback was previously dropped on the
2670
+ // floor β€” `patternsExtracted` reported 0 and `pattern-search` never
2671
+ // surfaced it. Step-less trajectories are the common case for LLM agents
2672
+ // (nothing forces step logging mid-task), and feedback is often the most
2673
+ // distilled lesson available. Route it through the same store + embed
2674
+ // path that pattern-store uses so it becomes searchable. Best-effort:
2675
+ // failures here must not turn the trajectory-end call itself into a
2676
+ // failure β€” the trajectory record was already persisted above.
2677
+ let feedbackDistilled = { stored: false };
2678
+ const hasSteps = !!trajectory && trajectory.steps.length > 0;
2679
+ const trimmedFeedback = typeof feedback === 'string' ? feedback.trim() : '';
2680
+ if (trajectory && !hasSteps && trimmedFeedback.length > 0) {
2681
+ const distilledPatternId = `pattern-feedback-${trajectoryId}-${Date.now()}`;
2682
+ const patternMetadata = {
2683
+ sourceTrajectoryId: trajectoryId,
2684
+ task: trajectory.task,
2685
+ agent: trajectory.agent,
2686
+ outcome: success ? 'success' : 'failure',
2687
+ distilledFrom: 'trajectory-end-feedback',
2688
+ };
2689
+ // Modest default confidence β€” step-less feedback hasn't been validated
2690
+ // by execution evidence the way a multi-step trajectory has.
2691
+ const feedbackConfidence = success ? 0.6 : 0.4;
2692
+ try {
2693
+ const bridge = await import('../memory/memory-bridge.js');
2694
+ const rb = await bridge.bridgeStorePattern({
2695
+ pattern: trimmedFeedback,
2696
+ type: 'trajectory-feedback',
2697
+ confidence: feedbackConfidence,
2698
+ metadata: patternMetadata,
2699
+ });
2700
+ if (rb?.success) {
2701
+ feedbackDistilled = { stored: true, patternId: rb.patternId, controller: rb.controller };
2702
+ }
2703
+ }
2704
+ catch {
2705
+ // Bridge unavailable β€” fall through to direct store
2706
+ }
2707
+ if (!feedbackDistilled.stored) {
2708
+ try {
2709
+ const storeFn = await getRealStoreFunction();
2710
+ if (storeFn) {
2711
+ const r = await storeFn({
2712
+ key: distilledPatternId,
2713
+ value: JSON.stringify({
2714
+ pattern: trimmedFeedback,
2715
+ type: 'trajectory-feedback',
2716
+ confidence: feedbackConfidence,
2717
+ metadata: patternMetadata,
2718
+ timestamp: endedAt,
2719
+ }),
2720
+ namespace: 'pattern',
2721
+ generateEmbeddingFlag: true,
2722
+ tags: [
2723
+ 'trajectory-feedback',
2724
+ success ? 'success' : 'failure',
2725
+ `confidence-${Math.round(feedbackConfidence * 100)}`,
2726
+ ],
2727
+ });
2728
+ if (r?.success) {
2729
+ feedbackDistilled = { stored: true, patternId: r.id || distilledPatternId, controller: 'store-fallback' };
2730
+ }
2731
+ }
2732
+ }
2733
+ catch {
2734
+ // Both paths failed β€” leave feedbackDistilled.stored = false.
2735
+ }
2736
+ }
2737
+ }
2668
2738
  const learningTimeMs = Date.now() - startTime;
2739
+ // patternsExtracted now reflects either recorded steps (the original
2740
+ // semantics) OR a distilled feedback pattern (#2351), so step-less
2741
+ // trajectories with useful feedback no longer report 0.
2742
+ const patternsExtracted = (trajectory?.steps.length || 0) + (feedbackDistilled.stored ? 1 : 0);
2669
2743
  return {
2670
2744
  trajectoryId,
2671
2745
  success,
@@ -2678,7 +2752,11 @@ export const hooksTrajectoryEnd = {
2678
2752
  sonaConfidence: sonaResult.confidence || undefined,
2679
2753
  ewcConsolidation: ewcResult.consolidated,
2680
2754
  ewcPenalty: ewcResult.penalty || undefined,
2681
- patternsExtracted: trajectory?.steps.length || 0,
2755
+ patternsExtracted,
2756
+ feedbackDistilled: feedbackDistilled.stored ? {
2757
+ patternId: feedbackDistilled.patternId,
2758
+ controller: feedbackDistilled.controller,
2759
+ } : undefined,
2682
2760
  learningTimeMs,
2683
2761
  globalStatsTrajectoriesDelta: globalStatsDelta, // Round B: was 0, now reflects
2684
2762
  },
@@ -10,7 +10,30 @@
10
10
  */
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
+ import { createRequire } from 'node:module';
13
14
  import { readFileMaybeEncrypted, writeFileRestricted } from '../fs-secure.js';
15
+ /**
16
+ * #2356 β€” cached, synchronous capability probe for @ruvector/core. `getHNSWStatus`
17
+ * is sync and is called by `neural status` in a fresh process that never warms
18
+ * the lazy HNSW singleton, so reporting availability off the warm singleton
19
+ * alone produced a false "Not loaded β€” @ruvector/core not available" even when
20
+ * the package is installed and exposes VectorDb. Resolving the module (without
21
+ * importing/initializing it) is a faithful, cheap availability signal.
22
+ */
23
+ let _ruvectorCoreResolvable;
24
+ function isRuvectorCoreResolvable() {
25
+ if (_ruvectorCoreResolvable !== undefined)
26
+ return _ruvectorCoreResolvable;
27
+ try {
28
+ const req = createRequire(import.meta.url);
29
+ req.resolve('@ruvector/core');
30
+ _ruvectorCoreResolvable = true;
31
+ }
32
+ catch {
33
+ _ruvectorCoreResolvable = false;
34
+ }
35
+ return _ruvectorCoreResolvable;
36
+ }
14
37
  /**
15
38
  * #1854: previously every site that needed the memory directory hardcoded
16
39
  * `getMemoryRoot()`, so the documented config entry
@@ -671,8 +694,13 @@ export function getHNSWStatus() {
671
694
  dimensions: hnswIndex?.dimensions ?? 384
672
695
  };
673
696
  }
697
+ // #2356: `available` now reflects real capability (index already loaded OR
698
+ // @ruvector/core installed and resolvable), not merely whether the lazy
699
+ // singleton happens to be warm in this process. `initialized` still reports
700
+ // whether the in-process index is actually loaded, so callers can tell
701
+ // "installed but not yet loaded" apart from "loaded".
674
702
  return {
675
- available: hnswIndex !== null,
703
+ available: hnswIndex !== null || isRuvectorCoreResolvable(),
676
704
  initialized: hnswIndex?.initialized ?? false,
677
705
  entryCount: hnswIndex?.entries.size ?? 0,
678
706
  dimensions: hnswIndex?.dimensions ?? 384
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Neural routing scaffold β€” `@ruvector/tiny-dancer` FastGRNN seam (#2334 Phase 1)
3
+ *
4
+ * Wires the optional neural path that ADR-026 originally described, behind a
5
+ * double gate that is OFF by default:
6
+ *
7
+ * CLAUDE_FLOW_ROUTER_NEURAL=1 β€” opt in to the neural path
8
+ * CLAUDE_FLOW_ROUTER_MODEL_PATH=<x.safetensors> β€” trained FastGRNN artifact
9
+ *
10
+ * Both must be set; otherwise `tryNeuralRoute` returns `null` immediately and
11
+ * the caller stays on the shipped heuristic + Thompson-bandit path. When the
12
+ * gate is open but anything fails (package not installed β€” it is an
13
+ * optionalDependency per ADR-124 β€” artifact missing/incompatible, runtime
14
+ * error), this module degrades gracefully: it returns `null`, never throws,
15
+ * and the caller reports `routedBy: 'bandit-fallback'` so the active path is
16
+ * observable rather than inferred from import success (ADR-086/074).
17
+ *
18
+ * Candidate modeling (#2334 Q3, provisional): the 3 model tiers are encoded as
19
+ * fixed candidates with deterministic placeholder embeddings (orthogonal-ish
20
+ * one-hot-block vectors). This is explicitly provisional β€” the trained Phase 2
21
+ * artifact defines what candidate embeddings mean, and this encoding is the
22
+ * scaffolding default until the maintainers answer #2334's candidate-modeling
23
+ * question. Until a real artifact exists the gate stays closed in practice, so
24
+ * the placeholder never influences routing.
25
+ *
26
+ * @module neural-router
27
+ */
28
+ /** The three routable tiers β€” 'inherit' is never a neural candidate. */
29
+ export type NeuralRoutableModel = 'haiku' | 'sonnet' | 'opus';
30
+ export interface NeuralRouteDecision {
31
+ model: NeuralRoutableModel;
32
+ confidence: number;
33
+ uncertainty: number;
34
+ inferenceTimeUs: number;
35
+ }
36
+ /** True when the user has opted in AND pointed at a model artifact. */
37
+ export declare function neuralRoutingEnabled(): boolean;
38
+ /** Reset cached state β€” for tests. */
39
+ export declare function resetNeuralRouter(): void;
40
+ /**
41
+ * Attempt a neural routing decision for the given task embedding.
42
+ *
43
+ * Returns `null` (never throws) when the gate is closed, the package or
44
+ * artifact is unavailable, or inference fails β€” callers fall back to the
45
+ * bandit and report `routedBy: 'bandit-fallback'` (when the gate was open)
46
+ * or `'heuristic'` (when it never was).
47
+ */
48
+ export declare function tryNeuralRoute(embedding: number[]): Promise<NeuralRouteDecision | null>;
49
+ //# sourceMappingURL=neural-router.d.ts.map
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Neural routing scaffold β€” `@ruvector/tiny-dancer` FastGRNN seam (#2334 Phase 1)
3
+ *
4
+ * Wires the optional neural path that ADR-026 originally described, behind a
5
+ * double gate that is OFF by default:
6
+ *
7
+ * CLAUDE_FLOW_ROUTER_NEURAL=1 β€” opt in to the neural path
8
+ * CLAUDE_FLOW_ROUTER_MODEL_PATH=<x.safetensors> β€” trained FastGRNN artifact
9
+ *
10
+ * Both must be set; otherwise `tryNeuralRoute` returns `null` immediately and
11
+ * the caller stays on the shipped heuristic + Thompson-bandit path. When the
12
+ * gate is open but anything fails (package not installed β€” it is an
13
+ * optionalDependency per ADR-124 β€” artifact missing/incompatible, runtime
14
+ * error), this module degrades gracefully: it returns `null`, never throws,
15
+ * and the caller reports `routedBy: 'bandit-fallback'` so the active path is
16
+ * observable rather than inferred from import success (ADR-086/074).
17
+ *
18
+ * Candidate modeling (#2334 Q3, provisional): the 3 model tiers are encoded as
19
+ * fixed candidates with deterministic placeholder embeddings (orthogonal-ish
20
+ * one-hot-block vectors). This is explicitly provisional β€” the trained Phase 2
21
+ * artifact defines what candidate embeddings mean, and this encoding is the
22
+ * scaffolding default until the maintainers answer #2334's candidate-modeling
23
+ * question. Until a real artifact exists the gate stays closed in practice, so
24
+ * the placeholder never influences routing.
25
+ *
26
+ * @module neural-router
27
+ */
28
+ import { existsSync } from 'fs';
29
+ // ============================================================================
30
+ // Gate & lifecycle
31
+ // ============================================================================
32
+ /** True when the user has opted in AND pointed at a model artifact. */
33
+ export function neuralRoutingEnabled() {
34
+ return process.env.CLAUDE_FLOW_ROUTER_NEURAL === '1'
35
+ && !!process.env.CLAUDE_FLOW_ROUTER_MODEL_PATH;
36
+ }
37
+ // Cached router instance + a sticky failure latch so a broken install/artifact
38
+ // costs one failed load, not one per routing call.
39
+ let routerInstance = null;
40
+ let loadFailed = false;
41
+ /** Reset cached state β€” for tests. */
42
+ export function resetNeuralRouter() {
43
+ routerInstance = null;
44
+ loadFailed = false;
45
+ }
46
+ async function loadRouter() {
47
+ if (routerInstance)
48
+ return routerInstance;
49
+ if (loadFailed)
50
+ return null;
51
+ const modelPath = process.env.CLAUDE_FLOW_ROUTER_MODEL_PATH;
52
+ if (!modelPath || !existsSync(modelPath)) {
53
+ loadFailed = true;
54
+ return null;
55
+ }
56
+ try {
57
+ // Dynamic import of an optionalDependency (ADR-124): absent on installs
58
+ // where the native binding failed or was skipped β€” degrade, don't throw.
59
+ const mod = await import('@ruvector/tiny-dancer');
60
+ const RouterCtor = mod.Router;
61
+ if (!RouterCtor) {
62
+ loadFailed = true;
63
+ return null;
64
+ }
65
+ routerInstance = new RouterCtor({ modelPath });
66
+ return routerInstance;
67
+ }
68
+ catch {
69
+ loadFailed = true;
70
+ return null;
71
+ }
72
+ }
73
+ // ============================================================================
74
+ // Candidate encoding (provisional β€” see header + #2334 Q3)
75
+ // ============================================================================
76
+ const TIER_ORDER = ['haiku', 'sonnet', 'opus'];
77
+ /**
78
+ * Deterministic placeholder embedding for a tier candidate: a block one-hot
79
+ * over the embedding dimensionality. Replaced by whatever the Phase 2 trained
80
+ * artifact defines as candidate space.
81
+ */
82
+ function tierCandidateEmbedding(tierIndex, dim) {
83
+ const v = new Array(dim).fill(0);
84
+ const block = Math.max(1, Math.floor(dim / TIER_ORDER.length));
85
+ const start = tierIndex * block;
86
+ for (let i = start; i < Math.min(start + block, dim); i++)
87
+ v[i] = 1 / Math.sqrt(block);
88
+ return v;
89
+ }
90
+ // ============================================================================
91
+ // Routing
92
+ // ============================================================================
93
+ /**
94
+ * Attempt a neural routing decision for the given task embedding.
95
+ *
96
+ * Returns `null` (never throws) when the gate is closed, the package or
97
+ * artifact is unavailable, or inference fails β€” callers fall back to the
98
+ * bandit and report `routedBy: 'bandit-fallback'` (when the gate was open)
99
+ * or `'heuristic'` (when it never was).
100
+ */
101
+ export async function tryNeuralRoute(embedding) {
102
+ if (!neuralRoutingEnabled())
103
+ return null;
104
+ if (!embedding || embedding.length === 0)
105
+ return null;
106
+ const router = await loadRouter();
107
+ if (!router)
108
+ return null;
109
+ try {
110
+ const response = await router.route({
111
+ queryEmbedding: embedding,
112
+ candidates: TIER_ORDER.map((tier, i) => ({
113
+ id: tier,
114
+ embedding: tierCandidateEmbedding(i, embedding.length),
115
+ metadata: JSON.stringify({ tier }),
116
+ })),
117
+ });
118
+ const best = response.decisions?.[0];
119
+ if (!best || !TIER_ORDER.includes(best.candidateId))
120
+ return null;
121
+ return {
122
+ model: best.candidateId,
123
+ confidence: best.confidence,
124
+ uncertainty: best.uncertainty,
125
+ inferenceTimeUs: response.inferenceTimeUs,
126
+ };
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ //# sourceMappingURL=neural-router.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Router trajectory collection (#2334 Phase 1)
3
+ *
4
+ * Opt-in per-decision dataset collection for the model router. The persisted
5
+ * bandit state (`.swarm/model-router-state.json`) keeps only aggregates β€”
6
+ * 9 Beta(Ξ±,Ξ²) cells and a capped/truncated history β€” which is not trainable
7
+ * material for the Phase 2 FastGRNN tier-classifier. This sidecar captures
8
+ * the per-example rows that training needs:
9
+ *
10
+ * decision rows: { taskHash, task, embedding?, complexity, features,
11
+ * model, confidence, uncertainty, routedBy, ts }
12
+ * outcome rows: { taskHash, model, outcome, ts }
13
+ *
14
+ * joined offline on `taskHash` (sha256-16 of the task text).
15
+ *
16
+ * OFF by default. Enable with CLAUDE_FLOW_ROUTER_TRAJECTORY=1. Rows append to
17
+ * `.swarm/model-router-trajectories.jsonl` β€” local-only, same trust domain as
18
+ * the existing state file, but unlike it the rows contain full task text (up
19
+ * to 500 chars) and raw embeddings, which is why this is opt-in rather than
20
+ * always-on.
21
+ *
22
+ * Writes are best-effort: any fs error is swallowed (collection must never
23
+ * break routing), matching the state-file behavior in model-router.ts.
24
+ *
25
+ * @module router-trajectory
26
+ */
27
+ export declare const TRAJECTORY_FILE = ".swarm/model-router-trajectories.jsonl";
28
+ export declare function trajectoryCollectionEnabled(): boolean;
29
+ /** Join key: first 16 hex chars of sha256(task). */
30
+ export declare function taskHash(task: string): string;
31
+ export interface TrajectoryDecisionRow {
32
+ v: number;
33
+ type: 'decision';
34
+ ts: string;
35
+ taskHash: string;
36
+ /** Task text, capped at 500 chars (cf. learningHistory's 100). */
37
+ task: string;
38
+ /** Raw embedding when one was threaded through route(); else omitted. */
39
+ embedding?: number[];
40
+ complexity: number;
41
+ features: {
42
+ lexicalComplexity: number;
43
+ semanticDepth: number;
44
+ taskScope: number;
45
+ uncertaintyLevel: number;
46
+ };
47
+ model: string;
48
+ confidence: number;
49
+ uncertainty: number;
50
+ routedBy: string;
51
+ }
52
+ export interface TrajectoryOutcomeRow {
53
+ v: number;
54
+ type: 'outcome';
55
+ ts: string;
56
+ taskHash: string;
57
+ model: string;
58
+ outcome: 'success' | 'failure' | 'escalated';
59
+ }
60
+ export declare function recordTrajectoryDecision(task: string, embedding: number[] | undefined, complexity: TrajectoryDecisionRow['features'] & {
61
+ score: number;
62
+ }, decision: {
63
+ model: string;
64
+ confidence: number;
65
+ uncertainty: number;
66
+ routedBy: string;
67
+ }): void;
68
+ export declare function recordTrajectoryOutcome(task: string, model: string, outcome: 'success' | 'failure' | 'escalated'): void;
69
+ //# sourceMappingURL=router-trajectory.d.ts.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Router trajectory collection (#2334 Phase 1)
3
+ *
4
+ * Opt-in per-decision dataset collection for the model router. The persisted
5
+ * bandit state (`.swarm/model-router-state.json`) keeps only aggregates β€”
6
+ * 9 Beta(Ξ±,Ξ²) cells and a capped/truncated history β€” which is not trainable
7
+ * material for the Phase 2 FastGRNN tier-classifier. This sidecar captures
8
+ * the per-example rows that training needs:
9
+ *
10
+ * decision rows: { taskHash, task, embedding?, complexity, features,
11
+ * model, confidence, uncertainty, routedBy, ts }
12
+ * outcome rows: { taskHash, model, outcome, ts }
13
+ *
14
+ * joined offline on `taskHash` (sha256-16 of the task text).
15
+ *
16
+ * OFF by default. Enable with CLAUDE_FLOW_ROUTER_TRAJECTORY=1. Rows append to
17
+ * `.swarm/model-router-trajectories.jsonl` β€” local-only, same trust domain as
18
+ * the existing state file, but unlike it the rows contain full task text (up
19
+ * to 500 chars) and raw embeddings, which is why this is opt-in rather than
20
+ * always-on.
21
+ *
22
+ * Writes are best-effort: any fs error is swallowed (collection must never
23
+ * break routing), matching the state-file behavior in model-router.ts.
24
+ *
25
+ * @module router-trajectory
26
+ */
27
+ import { createHash } from 'crypto';
28
+ import { appendFileSync, existsSync, mkdirSync } from 'fs';
29
+ import { dirname, join } from 'path';
30
+ export const TRAJECTORY_FILE = '.swarm/model-router-trajectories.jsonl';
31
+ /** Schema version stamped on every row so offline training can dispatch. */
32
+ const ROW_VERSION = 1;
33
+ export function trajectoryCollectionEnabled() {
34
+ return process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY === '1';
35
+ }
36
+ /** Join key: first 16 hex chars of sha256(task). */
37
+ export function taskHash(task) {
38
+ return createHash('sha256').update(task).digest('hex').slice(0, 16);
39
+ }
40
+ function appendRow(row) {
41
+ try {
42
+ const fullPath = join(process.cwd(), TRAJECTORY_FILE);
43
+ const dir = dirname(fullPath);
44
+ if (!existsSync(dir))
45
+ mkdirSync(dir, { recursive: true });
46
+ appendFileSync(fullPath, JSON.stringify(row) + '\n');
47
+ }
48
+ catch {
49
+ // Best-effort: collection must never break routing.
50
+ }
51
+ }
52
+ export function recordTrajectoryDecision(task, embedding, complexity, decision) {
53
+ if (!trajectoryCollectionEnabled())
54
+ return;
55
+ appendRow({
56
+ v: ROW_VERSION,
57
+ type: 'decision',
58
+ ts: new Date().toISOString(),
59
+ taskHash: taskHash(task),
60
+ task: task.slice(0, 500),
61
+ ...(embedding && embedding.length > 0 ? { embedding } : {}),
62
+ complexity: complexity.score,
63
+ features: {
64
+ lexicalComplexity: complexity.lexicalComplexity,
65
+ semanticDepth: complexity.semanticDepth,
66
+ taskScope: complexity.taskScope,
67
+ uncertaintyLevel: complexity.uncertaintyLevel,
68
+ },
69
+ model: decision.model,
70
+ confidence: decision.confidence,
71
+ uncertainty: decision.uncertainty,
72
+ routedBy: decision.routedBy,
73
+ });
74
+ }
75
+ export function recordTrajectoryOutcome(task, model, outcome) {
76
+ if (!trajectoryCollectionEnabled())
77
+ return;
78
+ appendRow({
79
+ v: ROW_VERSION,
80
+ type: 'outcome',
81
+ ts: new Date().toISOString(),
82
+ taskHash: taskHash(task),
83
+ model,
84
+ outcome,
85
+ });
86
+ }
87
+ //# sourceMappingURL=router-trajectory.js.map
@@ -55,6 +55,8 @@ export interface DaemonConfig {
55
55
  maxCpuLoad: number;
56
56
  minFreeMemoryPercent: number;
57
57
  };
58
+ ttlMs: number;
59
+ idleShutdownMs: number;
58
60
  workers: WorkerConfig[];
59
61
  }
60
62
  /**
@@ -65,6 +67,7 @@ export declare class WorkerDaemon extends EventEmitter {
65
67
  private workers;
66
68
  private timers;
67
69
  private queuePollTimer?;
70
+ private lifecycleTimer?;
68
71
  private running;
69
72
  private startedAt?;
70
73
  private projectRoot;
@@ -222,6 +225,30 @@ export declare class WorkerDaemon extends EventEmitter {
222
225
  * Stop the daemon and all workers
223
226
  */
224
227
  stop(): Promise<void>;
228
+ /**
229
+ * #2356 β€” Self-terminating lifecycle monitor. A daemon with no upper bound
230
+ * on its lifetime is the documented root cause of multi-day token leaks:
231
+ * each interval worker spawns a headless `claude --print` sweep, so a daemon
232
+ * left running for days dispatches tens of thousands of sessions invisibly.
233
+ * This timer enforces a max age (`ttlMs`) and an optional idle window
234
+ * (`idleShutdownMs`), shutting the daemon down gracefully when either trips.
235
+ * Checked once a minute and `unref()`'d so it never keeps the process alive
236
+ * on its own. A no-op when both limits are disabled (0).
237
+ */
238
+ private startLifecycleMonitor;
239
+ /**
240
+ * Most recent worker start/finish time across all workers (epoch ms), or
241
+ * null if no worker has ever started. Used for idle-shutdown detection.
242
+ */
243
+ private lastWorkerActivityMs;
244
+ /**
245
+ * Graceful self-shutdown triggered by the lifecycle monitor. Mirrors the
246
+ * signal-handler path (`stop()` then `process.exit(0)`) because the
247
+ * foreground keep-alive in the daemon command is a *ref'd* `setInterval`
248
+ * that would otherwise hold the process open after `stop()` clears the
249
+ * service timers β€” leaving a zombie that reports stopped but never exits.
250
+ */
251
+ private selfShutdown;
225
252
  /**
226
253
  * Get daemon status
227
254
  */
@@ -27,6 +27,30 @@ const DEFAULT_WORKERS = [
27
27
  // Worker timeout β€” must exceed the longest per-worker headless timeout (15 min for audit/refactor).
28
28
  // Previously 5 min, which caused orphan processes when daemon timeout fired before executor timeout (#1117).
29
29
  const DEFAULT_WORKER_TIMEOUT_MS = 16 * 60 * 1000;
30
+ // #2356 β€” Self-terminating lifecycle defaults. A background daemon with no
31
+ // upper bound on its lifetime runs until the box reboots; in the field this
32
+ // leaked tens of thousands of headless `claude --print` sweeps over many days
33
+ // (one observed daemon ran 19 days). A 12h default age cap (matching the
34
+ // pacphi/ruflo-machine-ref kit's proven value) heals a forgotten daemon within
35
+ // half a day; set RUFLO_DAEMON_TTL_SECS=0 (or `--ttl 0`) to opt out. Idle
36
+ // shutdown is opt-in (0 = disabled) since a legitimately quiet daemon is not a leak.
37
+ const DEFAULT_DAEMON_TTL_MS = 12 * 60 * 60 * 1000;
38
+ const DEFAULT_DAEMON_IDLE_SHUTDOWN_MS = 0;
39
+ /**
40
+ * Read a non-negative seconds value from an env var and return it as ms.
41
+ * Unlike the `parseInt(x) || default` idiom used elsewhere, an explicit `0`
42
+ * is honored (it disables the corresponding limit) rather than falling back
43
+ * to the default. Invalid / negative / absent values fall back.
44
+ */
45
+ function readEnvSecsAsMs(name, defaultMs) {
46
+ const raw = process.env[name];
47
+ if (raw === undefined || raw.trim() === '')
48
+ return defaultMs;
49
+ const secs = Number.parseInt(raw, 10);
50
+ if (!Number.isFinite(secs) || secs < 0)
51
+ return defaultMs;
52
+ return secs * 1000;
53
+ }
30
54
  /**
31
55
  * Worker Daemon - Manages background workers with Node.js
32
56
  */
@@ -37,6 +61,9 @@ export class WorkerDaemon extends EventEmitter {
37
61
  // #1845: separate timer for the MCP-dispatch queue poller. Kept off
38
62
  // the per-worker map so stop() clears both kinds without confusion.
39
63
  queuePollTimer;
64
+ // #2356: separate timer that enforces the daemon's max-age TTL + idle
65
+ // shutdown. Cleared in stop() alongside the worker/queue timers.
66
+ lifecycleTimer;
40
67
  running = false;
41
68
  startedAt;
42
69
  projectRoot;
@@ -92,6 +119,11 @@ export class WorkerDaemon extends EventEmitter {
92
119
  maxCpuLoad: config?.resourceThresholds?.maxCpuLoad ?? fileConfig.maxCpuLoad ?? smartMaxCpuLoad,
93
120
  minFreeMemoryPercent: config?.resourceThresholds?.minFreeMemoryPercent ?? fileConfig.minFreeMemoryPercent ?? defaultMinFreeMemory,
94
121
  },
122
+ // #2356 β€” precedence: constructor arg > config.json (daemon.ttlSecs) >
123
+ // env (RUFLO_DAEMON_TTL_SECS) > built-in default. readEnvSecsAsMs folds
124
+ // env-or-default and honors an explicit 0 (disable).
125
+ ttlMs: config?.ttlMs ?? fileConfig.ttlMs ?? readEnvSecsAsMs('RUFLO_DAEMON_TTL_SECS', DEFAULT_DAEMON_TTL_MS),
126
+ idleShutdownMs: config?.idleShutdownMs ?? fileConfig.idleShutdownMs ?? readEnvSecsAsMs('RUFLO_DAEMON_IDLE_SECS', DEFAULT_DAEMON_IDLE_SHUTDOWN_MS),
95
127
  workers: config?.workers ?? DEFAULT_WORKERS,
96
128
  };
97
129
  // Setup graceful shutdown handlers
@@ -274,12 +306,19 @@ export class WorkerDaemon extends EventEmitter {
274
306
  const rawMinMem = cfg['daemon.resourceThresholds.minFreeMemoryPercent'] ?? raw['daemon.resourceThresholds.minFreeMemoryPercent'];
275
307
  const rawMaxConcurrent = cfg['daemon.maxConcurrent'] ?? raw['daemon.maxConcurrent'];
276
308
  const rawTimeout = cfg['daemon.workerTimeoutMs'] ?? raw['daemon.workerTimeoutMs'];
309
+ // #2356 β€” lifecycle limits are configured in SECONDS in config.json
310
+ // (`daemon.ttlSecs` / `daemon.idleSecs`) for parity with the CLI flag
311
+ // and env var; stored internally as ms. An explicit 0 disables.
312
+ const rawTtl = cfg['daemon.ttlSecs'] ?? raw['daemon.ttlSecs'];
313
+ const rawIdle = cfg['daemon.idleSecs'] ?? raw['daemon.idleSecs'];
277
314
  return {
278
315
  autoStart: typeof raw['daemon.autoStart'] === 'boolean' ? raw['daemon.autoStart'] : undefined,
279
316
  maxConcurrent: (typeof rawMaxConcurrent === 'number' && rawMaxConcurrent > 0) ? rawMaxConcurrent : undefined,
280
317
  workerTimeoutMs: (typeof rawTimeout === 'number' && rawTimeout > 0) ? rawTimeout : undefined,
281
318
  maxCpuLoad: (typeof rawCpuLoad === 'number' && rawCpuLoad > 0 && rawCpuLoad < 1000) ? rawCpuLoad : undefined,
282
319
  minFreeMemoryPercent: (typeof rawMinMem === 'number' && rawMinMem >= 0 && rawMinMem <= 100) ? rawMinMem : undefined,
320
+ ttlMs: (typeof rawTtl === 'number' && rawTtl >= 0) ? rawTtl * 1000 : undefined,
321
+ idleShutdownMs: (typeof rawIdle === 'number' && rawIdle >= 0) ? rawIdle * 1000 : undefined,
283
322
  };
284
323
  }
285
324
  catch {
@@ -668,6 +707,9 @@ export class WorkerDaemon extends EventEmitter {
668
707
  if (typeof this.queuePollTimer.unref === 'function') {
669
708
  this.queuePollTimer.unref();
670
709
  }
710
+ // #2356: self-terminating lifecycle. Without an upper bound on lifetime a
711
+ // forgotten daemon keeps dispatching headless worker sweeps for days.
712
+ this.startLifecycleMonitor();
671
713
  // Save state
672
714
  this.saveState();
673
715
  this.log('info', `Daemon started (PID: ${process.pid}, CPUs: ${cpus().length}, workers: ${this.config.workers.filter(w => w.enabled).length}, maxCpuLoad: ${this.config.resourceThresholds.maxCpuLoad}, minFreeMemoryPercent: ${this.config.resourceThresholds.minFreeMemoryPercent}%)`);
@@ -762,12 +804,93 @@ export class WorkerDaemon extends EventEmitter {
762
804
  clearInterval(this.queuePollTimer);
763
805
  this.queuePollTimer = undefined;
764
806
  }
807
+ // #2356: stop the TTL/idle lifecycle monitor.
808
+ if (this.lifecycleTimer) {
809
+ clearInterval(this.lifecycleTimer);
810
+ this.lifecycleTimer = undefined;
811
+ }
765
812
  this.running = false;
766
813
  this.removePidFile();
767
814
  this.saveState();
768
815
  this.emit('stopped', { stoppedAt: new Date() });
769
816
  this.log('info', 'Daemon stopped');
770
817
  }
818
+ /**
819
+ * #2356 β€” Self-terminating lifecycle monitor. A daemon with no upper bound
820
+ * on its lifetime is the documented root cause of multi-day token leaks:
821
+ * each interval worker spawns a headless `claude --print` sweep, so a daemon
822
+ * left running for days dispatches tens of thousands of sessions invisibly.
823
+ * This timer enforces a max age (`ttlMs`) and an optional idle window
824
+ * (`idleShutdownMs`), shutting the daemon down gracefully when either trips.
825
+ * Checked once a minute and `unref()`'d so it never keeps the process alive
826
+ * on its own. A no-op when both limits are disabled (0).
827
+ */
828
+ startLifecycleMonitor() {
829
+ const ttlMs = this.config.ttlMs;
830
+ const idleMs = this.config.idleShutdownMs;
831
+ if ((!ttlMs || ttlMs <= 0) && (!idleMs || idleMs <= 0)) {
832
+ return; // both limits disabled β€” preserve legacy run-until-stopped behavior
833
+ }
834
+ const CHECK_INTERVAL_MS = 60_000;
835
+ this.lifecycleTimer = setInterval(() => {
836
+ if (!this.running)
837
+ return;
838
+ const now = Date.now();
839
+ const startedMs = this.startedAt?.getTime() ?? now;
840
+ if (ttlMs > 0 && now - startedMs >= ttlMs) {
841
+ void this.selfShutdown(`max age ${Math.round(ttlMs / 1000)}s reached`);
842
+ return;
843
+ }
844
+ if (idleMs > 0) {
845
+ const lastActivity = this.lastWorkerActivityMs() ?? startedMs;
846
+ if (now - lastActivity >= idleMs) {
847
+ void this.selfShutdown(`idle for ${Math.round(idleMs / 1000)}s (no worker activity)`);
848
+ }
849
+ }
850
+ }, CHECK_INTERVAL_MS);
851
+ if (typeof this.lifecycleTimer.unref === 'function') {
852
+ this.lifecycleTimer.unref();
853
+ }
854
+ const parts = [];
855
+ if (ttlMs > 0)
856
+ parts.push(`ttl=${Math.round(ttlMs / 1000)}s`);
857
+ if (idleMs > 0)
858
+ parts.push(`idle=${Math.round(idleMs / 1000)}s`);
859
+ this.log('info', `Lifecycle monitor active (${parts.join(', ')})`);
860
+ }
861
+ /**
862
+ * Most recent worker start/finish time across all workers (epoch ms), or
863
+ * null if no worker has ever started. Used for idle-shutdown detection.
864
+ */
865
+ lastWorkerActivityMs() {
866
+ let latest = null;
867
+ for (const state of this.workers.values()) {
868
+ for (const t of [state.lastRun, state.lastStartedAt]) {
869
+ if (t) {
870
+ const ms = t.getTime();
871
+ if (latest === null || ms > latest)
872
+ latest = ms;
873
+ }
874
+ }
875
+ }
876
+ return latest;
877
+ }
878
+ /**
879
+ * Graceful self-shutdown triggered by the lifecycle monitor. Mirrors the
880
+ * signal-handler path (`stop()` then `process.exit(0)`) because the
881
+ * foreground keep-alive in the daemon command is a *ref'd* `setInterval`
882
+ * that would otherwise hold the process open after `stop()` clears the
883
+ * service timers β€” leaving a zombie that reports stopped but never exits.
884
+ */
885
+ async selfShutdown(reason) {
886
+ this.log('info', `Daemon self-shutdown: ${reason}`);
887
+ this.emit('self-shutdown', { reason });
888
+ try {
889
+ await this.stop();
890
+ }
891
+ catch { /* best-effort β€” we are exiting regardless */ }
892
+ process.exit(0);
893
+ }
771
894
  /**
772
895
  * Get daemon status
773
896
  */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.10.41",
3
+ "version": "3.10.43",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",