claude-flow 3.5.23 → 3.5.25

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.25",
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'));
@@ -61,6 +61,8 @@ const commandLoaders = {
61
61
  guidance: () => import('./guidance.js'),
62
62
  // RVFA Appliance Management
63
63
  appliance: () => import('./appliance.js'),
64
+ 'appliance-advanced': () => import('./appliance-advanced.js'),
65
+ 'transfer-store': () => import('./transfer-store.js'),
64
66
  };
65
67
  // Cache for loaded commands
66
68
  const loadedCommands = new Map();
@@ -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
 
@@ -10,10 +10,10 @@ const browserSessions = new Map();
10
10
  * Execute agent-browser CLI command
11
11
  */
12
12
  async function execBrowserCommand(args, session = 'default') {
13
- const { execSync } = await import('child_process');
13
+ const { execFileSync } = await import('child_process');
14
14
  try {
15
15
  const fullArgs = ['--session', session, '--json', ...args];
16
- const result = execSync(`agent-browser ${fullArgs.join(' ')}`, {
16
+ const result = execFileSync('agent-browser', fullArgs, {
17
17
  encoding: 'utf-8',
18
18
  timeout: 30000,
19
19
  });
@@ -30,7 +30,7 @@ function getConfigPath() {
30
30
  function ensureConfigDir() {
31
31
  const dir = getConfigDir();
32
32
  if (!existsSync(dir)) {
33
- mkdirSync(dir, { recursive: true });
33
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
34
34
  }
35
35
  }
36
36
  function loadConfigStore() {
@@ -80,7 +80,16 @@ function filterDangerousKeys(obj) {
80
80
  return filtered;
81
81
  }
82
82
  function setNestedValue(obj, key, value) {
83
+ const MAX_NESTING_DEPTH = 10;
83
84
  const parts = key.split('.');
85
+ if (parts.length > MAX_NESTING_DEPTH) {
86
+ throw new Error(`Key exceeds maximum nesting depth of ${MAX_NESTING_DEPTH}`);
87
+ }
88
+ for (const part of parts) {
89
+ if (DANGEROUS_KEYS.has(part)) {
90
+ throw new Error(`Dangerous key segment rejected: ${part}`);
91
+ }
92
+ }
84
93
  let current = obj;
85
94
  for (let i = 0; i < parts.length - 1; i++) {
86
95
  const part = parts[i];
@@ -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 ? {
@@ -213,6 +213,7 @@ export const memoryTools = [
213
213
  const { getEntry } = await getMemoryFunctions();
214
214
  const key = input.key;
215
215
  const namespace = input.namespace || 'default';
216
+ validateMemoryInput(key);
216
217
  try {
217
218
  const result = await getEntry({ key, namespace });
218
219
  if (result.found && result.entry) {
@@ -337,6 +338,7 @@ export const memoryTools = [
337
338
  const { deleteEntry } = await getMemoryFunctions();
338
339
  const key = input.key;
339
340
  const namespace = input.namespace || 'default';
341
+ validateMemoryInput(key);
340
342
  try {
341
343
  const result = await deleteEntry({ key, namespace });
342
344
  return {
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Swarm MCP Tools for CLI
3
3
  *
4
- * Tool definitions for swarm coordination.
4
+ * Tool definitions for swarm coordination with file-based state persistence.
5
+ * Replaces previous stub implementations with real state tracking.
5
6
  */
6
7
  import type { MCPTool } from './types.js';
7
8
  export declare const swarmTools: MCPTool[];