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 +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +54 -7
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/init/executor.js +17 -17
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +10 -10
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/config-tools.js +10 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +150 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/swarm-tools.d.ts +2 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/swarm-tools.js +216 -30
- package/v3/@claude-flow/cli/dist/src/services/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/ruvector-training.js +11 -4
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +24 -3
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +123 -12
- package/v3/@claude-flow/cli/dist/src/transfer/storage/gcs.js +22 -6
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.5.
|
|
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
|
|
237
|
+
const spawnArgs = [
|
|
203
238
|
cliPath,
|
|
204
|
-
'daemon', 'start', '--foreground', '--quiet'
|
|
205
|
-
]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1560
|
-
claude mcp add
|
|
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 @
|
|
1575
|
-
npx @
|
|
1576
|
-
npx @
|
|
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 @
|
|
1580
|
-
npx @
|
|
1579
|
+
npx ruflo@latest swarm init --topology hierarchical --max-agents 8
|
|
1580
|
+
npx ruflo@latest swarm status
|
|
1581
1581
|
|
|
1582
1582
|
# Agents
|
|
1583
|
-
npx @
|
|
1584
|
-
npx @
|
|
1583
|
+
npx ruflo@latest agent spawn -t coder
|
|
1584
|
+
npx ruflo@latest agent list
|
|
1585
1585
|
|
|
1586
1586
|
# Memory
|
|
1587
|
-
npx @
|
|
1587
|
+
npx ruflo@latest memory search --query "patterns"
|
|
1588
1588
|
|
|
1589
1589
|
# Hooks
|
|
1590
|
-
npx @
|
|
1591
|
-
npx @
|
|
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
|
-
#
|
|
11
|
+
# Ruflo Pre-Commit Hook
|
|
12
12
|
# Validates code quality before commit
|
|
13
13
|
|
|
14
14
|
set -e
|
|
15
15
|
|
|
16
|
-
echo "🔍 Running
|
|
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
|
-
#
|
|
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
|
|
52
|
-
npx @
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
' *
|
|
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
|
-
*
|
|
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 {
|
|
13
|
+
const { execFileSync } = await import('child_process');
|
|
14
14
|
try {
|
|
15
15
|
const fullArgs = ['--session', session, '--json', ...args];
|
|
16
|
-
const result =
|
|
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
|
-
//
|
|
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(
|
|
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(
|
|
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 =
|
|
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: {
|
|
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[];
|