claude-flow 3.5.23 → 3.5.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.5.23",
3
+ "version": "3.5.24",
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",
@@ -19,6 +19,8 @@ const startCommand = {
19
19
  { name: 'foreground', short: 'f', type: 'boolean', description: 'Run daemon in foreground (blocks terminal)' },
20
20
  { name: 'headless', type: 'boolean', description: 'Enable headless worker execution (E2B sandbox)' },
21
21
  { name: 'sandbox', type: 'string', description: 'Default sandbox mode for headless workers', choices: ['strict', 'permissive', 'disabled'] },
22
+ { name: 'max-cpu-load', type: 'string', description: 'Override maxCpuLoad resource threshold (e.g. 4.0)' },
23
+ { name: 'min-free-memory', type: 'string', description: 'Override minFreeMemoryPercent resource threshold (e.g. 15)' },
22
24
  ],
23
25
  examples: [
24
26
  { command: 'claude-flow daemon start', description: 'Start daemon in background (default)' },
@@ -31,6 +33,37 @@ const startCommand = {
31
33
  const foreground = ctx.flags.foreground;
32
34
  const projectRoot = process.cwd();
33
35
  const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1';
36
+ // Parse resource threshold overrides from CLI flags
37
+ const config = {};
38
+ const rawMaxCpu = ctx.flags['max-cpu-load'];
39
+ const rawMinMem = ctx.flags['min-free-memory'];
40
+ // Strict numeric pattern to prevent command injection when forwarding to subprocess (S1)
41
+ const NUMERIC_RE = /^\d+(\.\d+)?$/;
42
+ const sanitize = (s) => s.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
43
+ if (rawMaxCpu || rawMinMem) {
44
+ const thresholds = {};
45
+ if (rawMaxCpu) {
46
+ const val = parseFloat(rawMaxCpu);
47
+ if (NUMERIC_RE.test(rawMaxCpu) && isFinite(val) && val > 0 && val <= 1000) {
48
+ thresholds.maxCpuLoad = val;
49
+ }
50
+ else if (!quiet) {
51
+ output.printWarning(`Ignoring invalid --max-cpu-load value: ${sanitize(rawMaxCpu)}`);
52
+ }
53
+ }
54
+ if (rawMinMem) {
55
+ const val = parseFloat(rawMinMem);
56
+ if (NUMERIC_RE.test(rawMinMem) && isFinite(val) && val >= 0 && val <= 100) {
57
+ thresholds.minFreeMemoryPercent = val;
58
+ }
59
+ else if (!quiet) {
60
+ output.printWarning(`Ignoring invalid --min-free-memory value: ${sanitize(rawMinMem)}`);
61
+ }
62
+ }
63
+ if (thresholds.maxCpuLoad !== undefined || thresholds.minFreeMemoryPercent !== undefined) {
64
+ config.resourceThresholds = thresholds;
65
+ }
66
+ }
34
67
  // Check if background daemon already running (skip if we ARE the daemon process)
35
68
  if (!isDaemonProcess) {
36
69
  const bgPid = getBackgroundDaemonPid(projectRoot);
@@ -43,7 +76,7 @@ const startCommand = {
43
76
  }
44
77
  // Background mode (default): fork a detached process
45
78
  if (!foreground) {
46
- return startBackgroundDaemon(projectRoot, quiet);
79
+ return startBackgroundDaemon(projectRoot, quiet, rawMaxCpu, rawMinMem);
47
80
  }
48
81
  // Foreground mode: run in current process (blocks terminal)
49
82
  try {
@@ -74,7 +107,7 @@ const startCommand = {
74
107
  if (!quiet) {
75
108
  const spinner = output.createSpinner({ text: 'Starting worker daemon...', spinner: 'dots' });
76
109
  spinner.start();
77
- const daemon = await startDaemon(projectRoot);
110
+ const daemon = await startDaemon(projectRoot, config);
78
111
  const status = daemon.getStatus();
79
112
  spinner.succeed('Worker daemon started (foreground mode)');
80
113
  output.writeln();
@@ -83,6 +116,8 @@ const startCommand = {
83
116
  `Started: ${status.startedAt?.toISOString()}`,
84
117
  `Workers: ${status.config.workers.filter(w => w.enabled).length} enabled`,
85
118
  `Max Concurrent: ${status.config.maxConcurrent}`,
119
+ `Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
120
+ `Min Free Memory: ${status.config.resourceThresholds.minFreeMemoryPercent}%`,
86
121
  ].join('\n'), 'Daemon Status');
87
122
  output.writeln();
88
123
  output.writeln(output.bold('Scheduled Workers'));
@@ -120,7 +155,7 @@ const startCommand = {
120
155
  await new Promise(() => { }); // Never resolves - daemon runs until killed
121
156
  }
122
157
  else {
123
- await startDaemon(projectRoot);
158
+ await startDaemon(projectRoot, config);
124
159
  await new Promise(() => { }); // Keep alive
125
160
  }
126
161
  return { success: true };
@@ -157,7 +192,7 @@ function validatePath(path, label) {
157
192
  /**
158
193
  * Start daemon as a detached background process
159
194
  */
160
- async function startBackgroundDaemon(projectRoot, quiet) {
195
+ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemory) {
161
196
  // Validate and resolve project root
162
197
  const resolvedRoot = resolve(projectRoot);
163
198
  validatePath(resolvedRoot, 'Project root');
@@ -199,10 +234,20 @@ async function startBackgroundDaemon(projectRoot, quiet) {
199
234
  };
200
235
  // Use spawn with explicit arguments instead of shell string interpolation
201
236
  // This prevents command injection via paths
202
- const child = spawn(process.execPath, [
237
+ const spawnArgs = [
203
238
  cliPath,
204
- 'daemon', 'start', '--foreground', '--quiet'
205
- ], spawnOpts);
239
+ 'daemon', 'start', '--foreground', '--quiet',
240
+ ];
241
+ // Forward resource threshold flags to the foreground child process
242
+ // Validate with strict numeric pattern to prevent shell injection on Windows (S1)
243
+ const SPAWN_NUMERIC_RE = /^\d+(\.\d+)?$/;
244
+ if (maxCpuLoad && SPAWN_NUMERIC_RE.test(maxCpuLoad)) {
245
+ spawnArgs.push('--max-cpu-load', maxCpuLoad);
246
+ }
247
+ if (minFreeMemory && SPAWN_NUMERIC_RE.test(minFreeMemory)) {
248
+ spawnArgs.push('--min-free-memory', minFreeMemory);
249
+ }
250
+ const child = spawn(process.execPath, spawnArgs, spawnOpts);
206
251
  // Get PID from spawned process directly (no shell echo needed)
207
252
  const pid = child.pid;
208
253
  if (!pid || pid <= 0) {
@@ -371,6 +416,8 @@ const statusCommand = {
371
416
  status.startedAt ? `Started: ${status.startedAt.toISOString()}` : '',
372
417
  `Workers Enabled: ${status.config.workers.filter(w => w.enabled).length}`,
373
418
  `Max Concurrent: ${status.config.maxConcurrent}`,
419
+ `Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
420
+ `Min Free Memory: ${status.config.resourceThresholds.minFreeMemoryPercent}%`,
374
421
  ].filter(Boolean).join('\n'), 'RuFlo Daemon');
375
422
  output.writeln();
376
423
  output.writeln(output.bold('Worker Status'));
@@ -430,7 +430,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
430
430
  ddd: { progress: 0, modules: 0, totalFiles: 0, totalLines: 0 },
431
431
  swarm: { activeAgents: 0, maxAgents: 15, topology: 'hierarchical-mesh' },
432
432
  learning: { status: 'READY', patternsLearned: 0, sessionsCompleted: 0 },
433
- _note: 'Metrics will update as you use Claude Flow'
433
+ _note: 'Metrics will update as you use Ruflo'
434
434
  };
435
435
  fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
436
436
  result.created.push('.claude-flow/metrics/v3-progress.json');
@@ -462,7 +462,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
462
462
  routing: { accuracy: 0, decisions: 0 },
463
463
  patterns: { shortTerm: 0, longTerm: 0, quality: 0 },
464
464
  sessions: { total: 0, current: null },
465
- _note: 'Intelligence grows as you use Claude Flow'
465
+ _note: 'Intelligence grows as you use Ruflo'
466
466
  };
467
467
  fs.writeFileSync(learningPath, JSON.stringify(learning, null, 2), 'utf-8');
468
468
  result.created.push('.claude-flow/metrics/learning.json');
@@ -1129,7 +1129,7 @@ async function writeInitialMetrics(targetDir, options, result) {
1129
1129
  patternsLearned: 0,
1130
1130
  sessionsCompleted: 0
1131
1131
  },
1132
- _note: 'Metrics will update as you use Claude Flow. Run: npx @claude-flow/cli@latest daemon start'
1132
+ _note: 'Metrics will update as you use Ruflo. Run: npx ruflo@latest daemon start'
1133
1133
  };
1134
1134
  fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf-8');
1135
1135
  result.created.files.push('.claude-flow/metrics/v3-progress.json');
@@ -1176,7 +1176,7 @@ async function writeInitialMetrics(targetDir, options, result) {
1176
1176
  total: 0,
1177
1177
  current: null
1178
1178
  },
1179
- _note: 'Intelligence grows as you use Claude Flow'
1179
+ _note: 'Intelligence grows as you use Ruflo'
1180
1180
  };
1181
1181
  fs.writeFileSync(learningPath, JSON.stringify(learning, null, 2), 'utf-8');
1182
1182
  result.created.files.push('.claude-flow/metrics/learning.json');
@@ -1197,7 +1197,7 @@ async function writeInitialMetrics(targetDir, options, result) {
1197
1197
  }
1198
1198
  }
1199
1199
  /**
1200
- * Write CAPABILITIES.md - comprehensive overview of all Claude Flow features
1200
+ * Write CAPABILITIES.md - comprehensive overview of all Ruflo features
1201
1201
  */
1202
1202
  async function writeCapabilitiesDoc(targetDir, options, result) {
1203
1203
  const capabilitiesPath = path.join(targetDir, '.claude-flow', 'CAPABILITIES.md');
@@ -1556,8 +1556,8 @@ npx @claude-flow/cli@latest hive-mind consensus --propose "task"
1556
1556
 
1557
1557
  ### MCP Server Setup
1558
1558
  \`\`\`bash
1559
- # Add Claude Flow MCP
1560
- claude mcp add claude-flow -- npx -y @claude-flow/cli@latest
1559
+ # Add Ruflo MCP
1560
+ claude mcp add ruflo -- npx -y ruflo@latest
1561
1561
 
1562
1562
  # Optional servers
1563
1563
  claude mcp add ruv-swarm -- npx -y ruv-swarm mcp start
@@ -1571,24 +1571,24 @@ claude mcp add flow-nexus -- npx -y flow-nexus@latest mcp start
1571
1571
  ### Essential Commands
1572
1572
  \`\`\`bash
1573
1573
  # Setup
1574
- npx @claude-flow/cli@latest init --wizard
1575
- npx @claude-flow/cli@latest daemon start
1576
- npx @claude-flow/cli@latest doctor --fix
1574
+ npx ruflo@latest init --wizard
1575
+ npx ruflo@latest daemon start
1576
+ npx ruflo@latest doctor --fix
1577
1577
 
1578
1578
  # Swarm
1579
- npx @claude-flow/cli@latest swarm init --topology hierarchical --max-agents 8
1580
- npx @claude-flow/cli@latest swarm status
1579
+ npx ruflo@latest swarm init --topology hierarchical --max-agents 8
1580
+ npx ruflo@latest swarm status
1581
1581
 
1582
1582
  # Agents
1583
- npx @claude-flow/cli@latest agent spawn -t coder
1584
- npx @claude-flow/cli@latest agent list
1583
+ npx ruflo@latest agent spawn -t coder
1584
+ npx ruflo@latest agent list
1585
1585
 
1586
1586
  # Memory
1587
- npx @claude-flow/cli@latest memory search --query "patterns"
1587
+ npx ruflo@latest memory search --query "patterns"
1588
1588
 
1589
1589
  # Hooks
1590
- npx @claude-flow/cli@latest hooks pre-task --description "task"
1591
- npx @claude-flow/cli@latest hooks worker dispatch --trigger optimize
1590
+ npx ruflo@latest hooks pre-task --description "task"
1591
+ npx ruflo@latest hooks worker dispatch --trigger optimize
1592
1592
  \`\`\`
1593
1593
 
1594
1594
  ### File Structure
@@ -8,12 +8,12 @@ import { generateStatuslineScript, generateStatuslineHook } from './statusline-g
8
8
  */
9
9
  export function generatePreCommitHook() {
10
10
  return `#!/bin/bash
11
- # Claude Flow Pre-Commit Hook
11
+ # Ruflo Pre-Commit Hook
12
12
  # Validates code quality before commit
13
13
 
14
14
  set -e
15
15
 
16
- echo "🔍 Running Claude Flow pre-commit checks..."
16
+ echo "🔍 Running Ruflo pre-commit checks..."
17
17
 
18
18
  # Get staged files
19
19
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
@@ -40,7 +40,7 @@ echo "✅ Pre-commit checks complete"
40
40
  */
41
41
  export function generatePostCommitHook() {
42
42
  return `#!/bin/bash
43
- # Claude Flow Post-Commit Hook
43
+ # Ruflo Post-Commit Hook
44
44
  # Records commit metrics and trains patterns
45
45
 
46
46
  COMMIT_HASH=$(git rev-parse HEAD)
@@ -48,8 +48,8 @@ COMMIT_MSG=$(git log -1 --pretty=%B)
48
48
 
49
49
  echo "📊 Recording commit metrics..."
50
50
 
51
- # Notify claude-flow of commit
52
- npx @claude-flow/cli hooks notify \\
51
+ # Notify ruflo of commit
52
+ npx ruflo@latest hooks notify \\
53
53
  --message "Commit: $COMMIT_MSG" \\
54
54
  --level info \\
55
55
  --metadata '{"hash": "'$COMMIT_HASH'"}' 2>/dev/null || true
@@ -63,7 +63,7 @@ echo "✅ Commit recorded"
63
63
  export function generateSessionManager() {
64
64
  return `#!/usr/bin/env node
65
65
  /**
66
- * Claude Flow Session Manager
66
+ * Ruflo Session Manager
67
67
  * Handles session lifecycle: start, restore, end
68
68
  */
69
69
 
@@ -196,7 +196,7 @@ module.exports = commands;
196
196
  export function generateAgentRouter() {
197
197
  return `#!/usr/bin/env node
198
198
  /**
199
- * Claude Flow Agent Router
199
+ * Ruflo Agent Router
200
200
  * Routes tasks to optimal agents based on learned patterns
201
201
  */
202
202
 
@@ -268,7 +268,7 @@ module.exports = { routeTask, AGENT_CAPABILITIES, TASK_PATTERNS };
268
268
  export function generateMemoryHelper() {
269
269
  return `#!/usr/bin/env node
270
270
  /**
271
- * Claude Flow Memory Helper
271
+ * Ruflo Memory Helper
272
272
  * Simple key-value memory for cross-session context
273
273
  */
274
274
 
@@ -361,7 +361,7 @@ export function generateHookHandler() {
361
361
  const lines = [
362
362
  '#!/usr/bin/env node',
363
363
  '/**',
364
- ' * Claude Flow Hook Handler (Cross-Platform)',
364
+ ' * Ruflo Hook Handler (Cross-Platform)',
365
365
  ' * Dispatches hook events to the appropriate helper modules.',
366
366
  ' */',
367
367
  '',
@@ -1026,7 +1026,7 @@ PowerShell -ExecutionPolicy Bypass -File "%~dp0daemon-manager.ps1" %*
1026
1026
  export function generateCrossPlatformSessionManager() {
1027
1027
  return `#!/usr/bin/env node
1028
1028
  /**
1029
- * Claude Flow Cross-Platform Session Manager
1029
+ * Ruflo Cross-Platform Session Manager
1030
1030
  * Works on Windows, macOS, and Linux
1031
1031
  */
1032
1032
 
@@ -3,7 +3,7 @@
3
3
  * Provides intelligent hooks functionality via MCP protocol
4
4
  */
5
5
  import { mkdirSync, writeFileSync, existsSync, readFileSync, statSync } from 'fs';
6
- import { join, resolve } from 'path';
6
+ import { dirname, join, resolve } from 'path';
7
7
  // Real vector search functions - lazy loaded to avoid circular imports
8
8
  let searchEntriesFn = null;
9
9
  async function getRealSearchFunction() {
@@ -121,7 +121,88 @@ function generateSimpleEmbedding(text, dimension = 384) {
121
121
  }
122
122
  return embedding;
123
123
  }
124
- // Task patterns used by both native and pure-JS routers
124
+ // ── Runtime routing outcome persistence ──────────────────────────────
125
+ // Closes the learning loop: post-task records outcomes → route loads them.
126
+ const ROUTING_OUTCOMES_PATH = join(resolve('.'), '.claude-flow/routing-outcomes.json');
127
+ const ROUTING_STOPWORDS = new Set([
128
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
129
+ 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'shall', 'can',
130
+ 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
131
+ 'before', 'after', 'above', 'below', 'between', 'under', 'again', 'further', 'then', 'once',
132
+ 'it', 'its', 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we', 'our', 'you', 'your',
133
+ 'he', 'she', 'they', 'them', 'and', 'but', 'or', 'nor', 'not', 'no', 'so', 'if', 'when', 'than',
134
+ 'very', 'just', 'also', 'only', 'both', 'each', 'all', 'any', 'few', 'more', 'most', 'other',
135
+ 'some', 'such', 'same', 'new', 'now', 'here', 'there', 'where', 'how', 'what', 'which', 'who',
136
+ ]);
137
+ function extractKeywords(text) {
138
+ if (!text)
139
+ return [];
140
+ return text.toLowerCase()
141
+ .replace(/[^a-z0-9\s-]/g, ' ')
142
+ .split(/\s+/)
143
+ .filter(w => w.length > 2 && !ROUTING_STOPWORDS.has(w));
144
+ }
145
+ function loadRoutingOutcomes() {
146
+ try {
147
+ if (existsSync(ROUTING_OUTCOMES_PATH)) {
148
+ const data = JSON.parse(readFileSync(ROUTING_OUTCOMES_PATH, 'utf-8'));
149
+ return data.outcomes || [];
150
+ }
151
+ }
152
+ catch { /* corrupt file, start fresh */ }
153
+ return [];
154
+ }
155
+ function saveRoutingOutcomes(outcomes) {
156
+ try {
157
+ const dir = dirname(ROUTING_OUTCOMES_PATH);
158
+ if (!existsSync(dir))
159
+ mkdirSync(dir, { recursive: true });
160
+ // Cap at 500 entries to bound file size
161
+ const capped = outcomes.slice(-500);
162
+ writeFileSync(ROUTING_OUTCOMES_PATH, JSON.stringify({ outcomes: capped }, null, 2));
163
+ }
164
+ catch { /* non-critical */ }
165
+ }
166
+ /**
167
+ * Build learned routing patterns from successful task outcomes.
168
+ * Returns patterns in the same shape as TASK_PATTERNS so they can be
169
+ * merged into both the native HNSW and pure-JS semantic routers.
170
+ */
171
+ function loadLearnedPatterns() {
172
+ const outcomes = loadRoutingOutcomes();
173
+ const byAgent = {};
174
+ for (const o of outcomes) {
175
+ if (!o.success || !o.agent || !o.keywords?.length)
176
+ continue;
177
+ if (!byAgent[o.agent])
178
+ byAgent[o.agent] = new Set();
179
+ for (const kw of o.keywords)
180
+ byAgent[o.agent].add(kw);
181
+ }
182
+ const patterns = {};
183
+ for (const [agent, kwSet] of Object.entries(byAgent)) {
184
+ patterns[`learned-${agent}`] = {
185
+ keywords: [...kwSet].slice(0, 50),
186
+ agents: [agent],
187
+ };
188
+ }
189
+ return patterns;
190
+ }
191
+ /**
192
+ * Merge static TASK_PATTERNS with runtime-learned patterns.
193
+ * Static patterns take precedence (learned patterns won't overwrite them).
194
+ */
195
+ function getMergedTaskPatterns() {
196
+ const merged = { ...TASK_PATTERNS };
197
+ const learned = loadLearnedPatterns();
198
+ for (const [key, pattern] of Object.entries(learned)) {
199
+ if (!merged[key]) {
200
+ merged[key] = pattern;
201
+ }
202
+ }
203
+ return merged;
204
+ }
205
+ // ── Static task patterns (used by both native and pure-JS routers) ───
125
206
  const TASK_PATTERNS = {
126
207
  'security-task': {
127
208
  keywords: ['authentication', 'security', 'auth', 'password', 'encryption', 'vulnerability', 'cve', 'audit'],
@@ -198,8 +279,8 @@ async function getSemanticRouter() {
198
279
  hnswEfConstruction: 200,
199
280
  hnswEfSearch: 100,
200
281
  });
201
- // Initialize with task patterns
202
- for (const [patternName, { keywords }] of Object.entries(TASK_PATTERNS)) {
282
+ // Initialize with static + runtime-learned task patterns
283
+ for (const [patternName, { keywords }] of Object.entries(getMergedTaskPatterns())) {
203
284
  for (const keyword of keywords) {
204
285
  const embedding = generateSimpleEmbedding(keyword);
205
286
  db.insert(`${patternName}:${keyword}`, embedding);
@@ -220,7 +301,7 @@ async function getSemanticRouter() {
220
301
  try {
221
302
  const { SemanticRouter } = await import('../ruvector/semantic-router.js');
222
303
  semanticRouter = new SemanticRouter({ dimension: 384 });
223
- for (const [patternName, { keywords, agents }] of Object.entries(TASK_PATTERNS)) {
304
+ for (const [patternName, { keywords, agents }] of Object.entries(getMergedTaskPatterns())) {
224
305
  const embeddings = keywords.map(kw => generateSimpleEmbedding(kw));
225
306
  semanticRouter.addIntentWithEmbeddings(patternName, embeddings, { agents, keywords });
226
307
  // Cache embeddings for keywords
@@ -414,11 +495,32 @@ function suggestAgentsForFile(filePath) {
414
495
  }
415
496
  function suggestAgentsForTask(task) {
416
497
  const taskLower = task.toLowerCase();
498
+ // Check static keyword patterns first
417
499
  for (const [pattern, result] of Object.entries(KEYWORD_PATTERNS)) {
418
500
  if (taskLower.includes(pattern)) {
419
501
  return result;
420
502
  }
421
503
  }
504
+ // Check runtime-learned patterns from successful task outcomes
505
+ const taskKeywords = extractKeywords(task);
506
+ if (taskKeywords.length > 0) {
507
+ const outcomes = loadRoutingOutcomes();
508
+ let bestAgent = '';
509
+ let bestOverlap = 0;
510
+ for (const outcome of outcomes) {
511
+ if (!outcome.success || !outcome.agent || !outcome.keywords?.length)
512
+ continue;
513
+ const overlap = taskKeywords.filter(kw => outcome.keywords.includes(kw)).length;
514
+ if (overlap > bestOverlap) {
515
+ bestOverlap = overlap;
516
+ bestAgent = outcome.agent;
517
+ }
518
+ }
519
+ // Require at least 2 keyword overlap to prevent false positives
520
+ if (bestAgent && bestOverlap >= 2) {
521
+ return { agents: [bestAgent], confidence: Math.min(0.6 + bestOverlap * 0.05, 0.85) };
522
+ }
523
+ }
422
524
  // Default fallback
423
525
  return { agents: ['coder', 'researcher', 'tester'], confidence: 0.7 };
424
526
  }
@@ -674,13 +776,16 @@ export const hooksRoute = {
674
776
  routingMethod = 'semantic-native';
675
777
  backendInfo = 'native VectorDb (HNSW)';
676
778
  // Convert results to semantic format
779
+ const mergedPatterns = getMergedTaskPatterns();
677
780
  semanticResult = results.map((r) => {
678
781
  const [patternName] = r.id.split(':');
679
- const pattern = TASK_PATTERNS[patternName];
782
+ const pattern = mergedPatterns[patternName];
680
783
  return {
681
784
  intent: patternName,
682
785
  score: 1 - r.score, // Native uses distance (lower is better), convert to similarity
683
- metadata: { agents: pattern?.agents || ['coder'] },
786
+ metadata: {
787
+ agents: pattern?.agents || (patternName.startsWith('learned-') ? [patternName.slice(8)] : ['coder']),
788
+ },
684
789
  };
685
790
  });
686
791
  }
@@ -941,6 +1046,8 @@ export const hooksPostTask = {
941
1046
  success: { type: 'boolean', description: 'Whether task was successful' },
942
1047
  agent: { type: 'string', description: 'Agent that completed the task' },
943
1048
  quality: { type: 'number', description: 'Quality score (0-1)' },
1049
+ task: { type: 'string', description: 'Task description text (used for learning keyword extraction)' },
1050
+ storeDecisions: { type: 'boolean', description: 'Also store routing decision in memory DB' },
944
1051
  },
945
1052
  required: ['taskId'],
946
1053
  },
@@ -979,6 +1086,41 @@ export const hooksPostTask = {
979
1086
  catch {
980
1087
  // Non-fatal
981
1088
  }
1089
+ // Persist routing outcome for runtime learning (file-based, always reliable)
1090
+ const taskText = params.task || '';
1091
+ const outcomeKeywords = extractKeywords(taskText);
1092
+ let outcomePersisted = false;
1093
+ if (taskText && agent && agent.length <= 100 && /^[a-zA-Z0-9_-]+$/.test(agent)) {
1094
+ try {
1095
+ const outcomes = loadRoutingOutcomes();
1096
+ outcomes.push({
1097
+ task: taskText,
1098
+ agent,
1099
+ success,
1100
+ quality,
1101
+ keywords: outcomeKeywords,
1102
+ timestamp: new Date().toISOString(),
1103
+ });
1104
+ saveRoutingOutcomes(outcomes);
1105
+ outcomePersisted = true;
1106
+ }
1107
+ catch { /* non-critical */ }
1108
+ }
1109
+ // Optionally store in memory DB for cross-session vector retrieval
1110
+ if (params.storeDecisions && taskText && agent) {
1111
+ try {
1112
+ const storeFn = await getRealStoreFunction();
1113
+ if (storeFn) {
1114
+ await storeFn({
1115
+ key: `routing-decision:${taskId}`,
1116
+ namespace: 'patterns',
1117
+ value: JSON.stringify({ task: taskText, agent, success, quality, keywords: outcomeKeywords }),
1118
+ tags: ['routing-decision'],
1119
+ });
1120
+ }
1121
+ }
1122
+ catch { /* non-critical */ }
1123
+ }
982
1124
  const duration = Date.now() - startTime;
983
1125
  return {
984
1126
  taskId,
@@ -989,6 +1131,7 @@ export const hooksPostTask = {
989
1131
  newPatterns: success ? 1 : 0,
990
1132
  trajectoryId: `traj-${Date.now()}`,
991
1133
  controller: feedbackResult?.controller || 'none',
1134
+ outcomePersisted,
992
1135
  },
993
1136
  quality,
994
1137
  feedback: feedbackResult ? {
@@ -6,7 +6,7 @@ export { WorkerDaemon, getDaemon, startDaemon, stopDaemon, type WorkerType, } fr
6
6
  export { HeadlessWorkerExecutor, HEADLESS_WORKER_TYPES, HEADLESS_WORKER_CONFIGS, LOCAL_WORKER_TYPES, LOCAL_WORKER_CONFIGS, ALL_WORKER_CONFIGS, isHeadlessWorker, isLocalWorker, getModelId, getWorkerConfig, type HeadlessWorkerType, type LocalWorkerType, type HeadlessWorkerConfig, type HeadlessExecutionResult, type HeadlessExecutorConfig, type HeadlessOptions, type PoolStatus, type SandboxMode, type ModelType, type OutputFormat, type ExecutionMode, type WorkerPriority, type WorkerConfig, } from './headless-worker-executor.js';
7
7
  export { ContainerWorkerPool, type ContainerInfo, type ContainerPoolConfig, type ContainerExecutionOptions, type ContainerPoolStatus, type ContainerState, } from './container-worker-pool.js';
8
8
  export { WorkerQueue, type QueueTask, type WorkerQueueConfig, type QueueStats, type WorkerRegistration, type TaskStatus, } from './worker-queue.js';
9
- export type { default as WorkerDaemonType } from './worker-daemon.js';
9
+ export type { default as WorkerDaemonType, DaemonConfig } from './worker-daemon.js';
10
10
  export type { default as HeadlessWorkerExecutorType } from './headless-worker-executor.js';
11
11
  export type { default as ContainerWorkerPoolType } from './container-worker-pool.js';
12
12
  export type { default as WorkerQueueType } from './worker-queue.js';
@@ -13,6 +13,14 @@
13
13
  *
14
14
  * Created with ❤️ by ruv.io
15
15
  */
16
+ /**
17
+ * ESM/CJS interop helper — handles `.default` for CJS modules.
18
+ * Uses `'default' in mod` check which is safer than `mod.default || mod`.
19
+ */
20
+ async function importWithInterop(packageName) {
21
+ const mod = await import(packageName);
22
+ return ('default' in mod) ? mod.default : mod;
23
+ }
16
24
  // Lazy-loaded WASM modules
17
25
  let microLoRA = null;
18
26
  let scopedLoRA = null;
@@ -252,7 +260,7 @@ export async function initializeTraining(config = {}) {
252
260
  }
253
261
  // --- Attention mechanisms (optional, independent of WASM) ---
254
262
  try {
255
- const attention = await import('@ruvector/attention');
263
+ const attention = await importWithInterop('@ruvector/attention');
256
264
  if (config.useFlashAttention !== false) {
257
265
  flashAttention = new attention.FlashAttention(dim, 64);
258
266
  features.push('FlashAttention');
@@ -289,9 +297,8 @@ export async function initializeTraining(config = {}) {
289
297
  // --- SONA (optional, backward compatible) ---
290
298
  if (config.useSona !== false) {
291
299
  try {
292
- const sona = await import('@ruvector/sona');
300
+ const sona = await importWithInterop('@ruvector/sona');
293
301
  const sonaRank = config.sonaRank || 4;
294
- // @ts-expect-error - SonaEngine accepts 4 positional args but types say 1
295
302
  sonaEngine = new sona.SonaEngine(dim, sonaRank, alpha, lr);
296
303
  sonaAvailable = true;
297
304
  features.push(`SONA (${dim}-dim, rank-${sonaRank}, 624k learn/s)`);
@@ -473,7 +480,7 @@ export function mineHardNegatives(anchor, candidates) {
473
480
  * Benchmark the training system
474
481
  */
475
482
  export async function benchmarkTraining(dim, iterations) {
476
- const attention = await import('@ruvector/attention');
483
+ const attention = await importWithInterop('@ruvector/attention');
477
484
  lastBenchmark = attention.benchmarkAttention(dim || 256, 100, iterations || 1000);
478
485
  return lastBenchmark ?? [];
479
486
  }
@@ -44,7 +44,7 @@ interface DaemonStatus {
44
44
  workers: Map<WorkerType, WorkerState>;
45
45
  config: DaemonConfig;
46
46
  }
47
- interface DaemonConfig {
47
+ export interface DaemonConfig {
48
48
  autoStart: boolean;
49
49
  logDir: string;
50
50
  stateFile: string;
@@ -70,6 +70,7 @@ export declare class WorkerDaemon extends EventEmitter {
70
70
  private pendingWorkers;
71
71
  private headlessExecutor;
72
72
  private headlessAvailable;
73
+ private originalConfig?;
73
74
  constructor(projectRoot: string, config?: Partial<DaemonConfig>);
74
75
  /**
75
76
  * Initialize headless executor if Claude Code is available
@@ -83,6 +84,20 @@ export declare class WorkerDaemon extends EventEmitter {
83
84
  * Get headless executor instance
84
85
  */
85
86
  getHeadlessExecutor(): HeadlessWorkerExecutor | null;
87
+ /**
88
+ * Detect effective CPU count for the current environment.
89
+ *
90
+ * Inside Docker / K8s containers, os.cpus().length reports the HOST cpu
91
+ * count, not the container limit (Node.js #28762 — wontfix). We read
92
+ * cgroup v2 / v1 quota files first so the maxCpuLoad threshold stays
93
+ * meaningful under resource-limited containers.
94
+ */
95
+ static getEffectiveCpuCount(): number;
96
+ /**
97
+ * Read daemon-specific config from .claude-flow/config.json
98
+ * Supports dot-notation keys like 'daemon.resourceThresholds.maxCpuLoad'
99
+ */
100
+ private readDaemonConfigFromFile;
86
101
  /**
87
102
  * Setup graceful shutdown handlers
88
103
  */
@@ -93,6 +108,12 @@ export declare class WorkerDaemon extends EventEmitter {
93
108
  private canRunWorker;
94
109
  /**
95
110
  * Process pending workers queue
111
+ *
112
+ * When executeWorkerWithConcurrencyControl defers a worker (returns null),
113
+ * we break immediately to avoid a busy-wait loop — the deferred worker is
114
+ * already back on the pendingWorkers queue by that point. If no workers are
115
+ * currently running when we break, we schedule a backoff retry so the queue
116
+ * does not get permanently stuck.
96
117
  */
97
118
  private processPendingWorkers;
98
119
  private initializeWorkerStates;
@@ -194,11 +215,11 @@ export declare class WorkerDaemon extends EventEmitter {
194
215
  /**
195
216
  * Get or create daemon instance
196
217
  */
197
- export declare function getDaemon(projectRoot?: string): WorkerDaemon;
218
+ export declare function getDaemon(projectRoot?: string, config?: Partial<DaemonConfig>): WorkerDaemon;
198
219
  /**
199
220
  * Start daemon (for use in session-start hook)
200
221
  */
201
- export declare function startDaemon(projectRoot: string): Promise<WorkerDaemon>;
222
+ export declare function startDaemon(projectRoot: string, config?: Partial<DaemonConfig>): Promise<WorkerDaemon>;
202
223
  /**
203
224
  * Stop daemon
204
225
  */
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { EventEmitter } from 'events';
13
13
  import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'fs';
14
+ import { cpus } from 'os';
14
15
  import { join } from 'path';
15
16
  import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
16
17
  // Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
@@ -40,19 +41,35 @@ export class WorkerDaemon extends EventEmitter {
40
41
  // Headless execution support
41
42
  headlessExecutor = null;
42
43
  headlessAvailable = false;
44
+ // Preserve the original constructor config so we can detect explicit overrides
45
+ // during state restoration (R1: constructor config takes priority over stale state)
46
+ originalConfig;
43
47
  constructor(projectRoot, config) {
44
48
  super();
45
49
  this.projectRoot = projectRoot;
50
+ this.originalConfig = config;
46
51
  const claudeFlowDir = join(projectRoot, '.claude-flow');
52
+ // Read daemon config from .claude-flow/config.json (Layer B)
53
+ const fileConfig = this.readDaemonConfigFromFile(claudeFlowDir);
54
+ // CPU-proportional smart default instead of hardcoded 2.0
55
+ const cpuCount = WorkerDaemon.getEffectiveCpuCount();
56
+ const smartMaxCpuLoad = Math.max(cpuCount * 0.8, 2.0); // Floor of 2.0 for single-CPU machines
57
+ // Platform-aware default: macOS os.freemem() excludes reclaimable file cache,
58
+ // so reported "free" is much lower than actually available memory.
59
+ // Linux reports available memory (including reclaimable cache) more accurately.
60
+ const defaultMinFreeMemory = process.platform === 'darwin' ? 5 : 10;
61
+ // Priority: constructor arg > config.json > smart default
62
+ // For resourceThresholds, merge field-by-field so partial overrides
63
+ // (e.g. only --max-cpu-load) still pick up defaults for other fields.
47
64
  this.config = {
48
- autoStart: config?.autoStart ?? false, // P1 fix: Default to false for explicit consent
65
+ autoStart: config?.autoStart ?? fileConfig.autoStart ?? false,
49
66
  logDir: config?.logDir ?? join(claudeFlowDir, 'logs'),
50
67
  stateFile: config?.stateFile ?? join(claudeFlowDir, 'daemon-state.json'),
51
- maxConcurrent: config?.maxConcurrent ?? 2, // P0 fix: Limit concurrent workers
52
- workerTimeoutMs: config?.workerTimeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS,
53
- resourceThresholds: config?.resourceThresholds ?? {
54
- maxCpuLoad: 2.0,
55
- minFreeMemoryPercent: 20,
68
+ maxConcurrent: config?.maxConcurrent ?? fileConfig.maxConcurrent ?? 2,
69
+ workerTimeoutMs: config?.workerTimeoutMs ?? fileConfig.workerTimeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS,
70
+ resourceThresholds: {
71
+ maxCpuLoad: config?.resourceThresholds?.maxCpuLoad ?? fileConfig.maxCpuLoad ?? smartMaxCpuLoad,
72
+ minFreeMemoryPercent: config?.resourceThresholds?.minFreeMemoryPercent ?? fileConfig.minFreeMemoryPercent ?? defaultMinFreeMemory,
56
73
  },
57
74
  workers: config?.workers ?? DEFAULT_WORKERS,
58
75
  };
@@ -118,6 +135,66 @@ export class WorkerDaemon extends EventEmitter {
118
135
  getHeadlessExecutor() {
119
136
  return this.headlessExecutor;
120
137
  }
138
+ /**
139
+ * Detect effective CPU count for the current environment.
140
+ *
141
+ * Inside Docker / K8s containers, os.cpus().length reports the HOST cpu
142
+ * count, not the container limit (Node.js #28762 — wontfix). We read
143
+ * cgroup v2 / v1 quota files first so the maxCpuLoad threshold stays
144
+ * meaningful under resource-limited containers.
145
+ */
146
+ static getEffectiveCpuCount() {
147
+ // 1. Try cgroup v2: /sys/fs/cgroup/cpu.max
148
+ try {
149
+ const cpuMax = readFileSync('/sys/fs/cgroup/cpu.max', 'utf8').trim();
150
+ const [quotaStr, periodStr] = cpuMax.split(' ');
151
+ if (quotaStr !== 'max') {
152
+ const quota = parseInt(quotaStr, 10);
153
+ const period = parseInt(periodStr, 10);
154
+ if (quota > 0 && period > 0)
155
+ return Math.ceil(quota / period);
156
+ }
157
+ }
158
+ catch { /* not in cgroup v2 */ }
159
+ // 2. Try cgroup v1: /sys/fs/cgroup/cpu/cpu.cfs_quota_us
160
+ try {
161
+ const quota = parseInt(readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8').trim(), 10);
162
+ const period = parseInt(readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8').trim(), 10);
163
+ if (quota > 0 && period > 0)
164
+ return Math.ceil(quota / period);
165
+ }
166
+ catch { /* not in cgroup v1 */ }
167
+ // 3. Fallback to os.cpus().length
168
+ return cpus().length || 1;
169
+ }
170
+ /**
171
+ * Read daemon-specific config from .claude-flow/config.json
172
+ * Supports dot-notation keys like 'daemon.resourceThresholds.maxCpuLoad'
173
+ */
174
+ readDaemonConfigFromFile(claudeFlowDir) {
175
+ const configPath = join(claudeFlowDir, 'config.json');
176
+ if (!existsSync(configPath))
177
+ return {};
178
+ try {
179
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
180
+ // Support both flat keys at root and nested under scopes.project
181
+ const cfg = raw?.scopes?.project ?? raw;
182
+ const rawCpuLoad = cfg['daemon.resourceThresholds.maxCpuLoad'] ?? raw['daemon.resourceThresholds.maxCpuLoad'];
183
+ const rawMinMem = cfg['daemon.resourceThresholds.minFreeMemoryPercent'] ?? raw['daemon.resourceThresholds.minFreeMemoryPercent'];
184
+ const rawMaxConcurrent = cfg['daemon.maxConcurrent'] ?? raw['daemon.maxConcurrent'];
185
+ const rawTimeout = cfg['daemon.workerTimeoutMs'] ?? raw['daemon.workerTimeoutMs'];
186
+ return {
187
+ autoStart: typeof raw['daemon.autoStart'] === 'boolean' ? raw['daemon.autoStart'] : undefined,
188
+ maxConcurrent: (typeof rawMaxConcurrent === 'number' && rawMaxConcurrent > 0) ? rawMaxConcurrent : undefined,
189
+ workerTimeoutMs: (typeof rawTimeout === 'number' && rawTimeout > 0) ? rawTimeout : undefined,
190
+ maxCpuLoad: (typeof rawCpuLoad === 'number' && rawCpuLoad > 0 && rawCpuLoad < 1000) ? rawCpuLoad : undefined,
191
+ minFreeMemoryPercent: (typeof rawMinMem === 'number' && rawMinMem >= 0 && rawMinMem <= 100) ? rawMinMem : undefined,
192
+ };
193
+ }
194
+ catch {
195
+ return {};
196
+ }
197
+ }
121
198
  /**
122
199
  * Setup graceful shutdown handlers
123
200
  */
@@ -150,13 +227,30 @@ export class WorkerDaemon extends EventEmitter {
150
227
  }
151
228
  /**
152
229
  * Process pending workers queue
230
+ *
231
+ * When executeWorkerWithConcurrencyControl defers a worker (returns null),
232
+ * we break immediately to avoid a busy-wait loop — the deferred worker is
233
+ * already back on the pendingWorkers queue by that point. If no workers are
234
+ * currently running when we break, we schedule a backoff retry so the queue
235
+ * does not get permanently stuck.
153
236
  */
154
237
  async processPendingWorkers() {
155
238
  while (this.pendingWorkers.length > 0 && this.runningWorkers.size < this.config.maxConcurrent) {
156
239
  const workerType = this.pendingWorkers.shift();
157
240
  const workerConfig = this.config.workers.find(w => w.type === workerType);
158
241
  if (workerConfig) {
159
- await this.executeWorkerWithConcurrencyControl(workerConfig);
242
+ const result = await this.executeWorkerWithConcurrencyControl(workerConfig);
243
+ if (result === null) {
244
+ // Worker was deferred (resource pressure or concurrency limit).
245
+ // Break to avoid tight-looping — the next executeWorker() completion
246
+ // will call processPendingWorkers() again via the finally block.
247
+ if (this.runningWorkers.size === 0) {
248
+ // No workers running means nobody will trigger the finally-block
249
+ // callback, so schedule a backoff retry to avoid a stuck queue.
250
+ setTimeout(() => this.processPendingWorkers(), 30_000).unref();
251
+ }
252
+ break;
253
+ }
160
254
  }
161
255
  }
162
256
  }
@@ -175,6 +269,23 @@ export class WorkerDaemon extends EventEmitter {
175
269
  }
176
270
  }
177
271
  }
272
+ // Restore resourceThresholds, maxConcurrent, workerTimeoutMs from saved state
273
+ // Only restore if valid numeric values within sane ranges
274
+ if (saved.config?.resourceThresholds && !this.originalConfig?.resourceThresholds) {
275
+ const rt = saved.config.resourceThresholds;
276
+ if (typeof rt.maxCpuLoad === 'number' && rt.maxCpuLoad > 0 && rt.maxCpuLoad < 1000) {
277
+ this.config.resourceThresholds.maxCpuLoad = rt.maxCpuLoad;
278
+ }
279
+ if (typeof rt.minFreeMemoryPercent === 'number' && rt.minFreeMemoryPercent >= 0 && rt.minFreeMemoryPercent <= 100) {
280
+ this.config.resourceThresholds.minFreeMemoryPercent = rt.minFreeMemoryPercent;
281
+ }
282
+ }
283
+ if (typeof saved.config?.maxConcurrent === 'number' && saved.config.maxConcurrent > 0) {
284
+ this.config.maxConcurrent = saved.config.maxConcurrent;
285
+ }
286
+ if (typeof saved.config?.workerTimeoutMs === 'number' && saved.config.workerTimeoutMs > 0) {
287
+ this.config.workerTimeoutMs = saved.config.workerTimeoutMs;
288
+ }
178
289
  // Restore worker runtime states (runCount, successCount, etc.)
179
290
  if (saved.workers) {
180
291
  for (const [type, state] of Object.entries(saved.workers)) {
@@ -228,7 +339,7 @@ export class WorkerDaemon extends EventEmitter {
228
339
  }
229
340
  // Save state
230
341
  this.saveState();
231
- this.log('info', `Daemon started with ${this.config.workers.filter(w => w.enabled).length} workers`);
342
+ 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}%)`);
232
343
  }
233
344
  /**
234
345
  * Stop the daemon and all workers
@@ -755,9 +866,9 @@ let daemonInstance = null;
755
866
  /**
756
867
  * Get or create daemon instance
757
868
  */
758
- export function getDaemon(projectRoot) {
869
+ export function getDaemon(projectRoot, config) {
759
870
  if (!daemonInstance && projectRoot) {
760
- daemonInstance = new WorkerDaemon(projectRoot);
871
+ daemonInstance = new WorkerDaemon(projectRoot, config);
761
872
  }
762
873
  if (!daemonInstance) {
763
874
  throw new Error('Daemon not initialized. Provide projectRoot on first call.');
@@ -767,8 +878,8 @@ export function getDaemon(projectRoot) {
767
878
  /**
768
879
  * Start daemon (for use in session-start hook)
769
880
  */
770
- export async function startDaemon(projectRoot) {
771
- const daemon = getDaemon(projectRoot);
881
+ export async function startDaemon(projectRoot, config) {
882
+ const daemon = getDaemon(projectRoot, config);
772
883
  await daemon.start();
773
884
  return daemon;
774
885
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.5.23",
3
+ "version": "3.5.24",
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",