@claude-flow/cli 3.32.34 → 3.32.36
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/.claude/helpers/helpers.manifest.json +5 -5
- package/.claude/helpers/hook-handler.cjs +30 -0
- package/.claude/helpers/intelligence.cjs +47 -4
- package/.claude/helpers/statusline.cjs +169 -6
- package/bin/cli.js +25 -1
- package/catalog-manifest.json +4 -4
- package/dist/src/commands/agent.js +50 -2
- package/dist/src/commands/doctor.js +159 -11
- package/dist/src/commands/hooks.js +63 -4
- package/dist/src/commands/mcp.js +1 -1
- package/dist/src/commands/memory-distill.js +1 -0
- package/dist/src/commands/metaharness.js +4 -0
- package/dist/src/commands/swarm.js +89 -5
- package/dist/src/config-adapter.js +1 -0
- package/dist/src/index.js +3 -2
- package/dist/src/init/executor.js +16 -10
- package/dist/src/init/helpers-generator.js +15 -4
- package/dist/src/init/statusline-generator.js +18 -5
- package/dist/src/mcp-server.d.ts +25 -0
- package/dist/src/mcp-server.js +57 -2
- package/dist/src/mcp-tools/agent-tools.js +10 -0
- package/dist/src/mcp-tools/embeddings-tools.js +51 -10
- package/dist/src/mcp-tools/hooks-tools.js +120 -60
- package/dist/src/mcp-tools/memory-tools.js +14 -2
- package/dist/src/mcp-tools/metaharness-tools.js +7 -0
- package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
- package/dist/src/mcp-tools/swarm-tools.js +172 -7
- package/dist/src/memory/memory-bridge.d.ts +2 -0
- package/dist/src/memory/memory-bridge.js +5 -1
- package/dist/src/memory/memory-initializer.d.ts +9 -1
- package/dist/src/memory/memory-initializer.js +66 -19
- package/dist/src/services/daemon-autostart.js +7 -4
- package/dist/src/services/flywheel-receipt.d.ts +3 -0
- package/dist/src/services/flywheel-receipt.js +1 -0
- package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
- package/dist/src/services/harness-flywheel-runtime.js +19 -19
- package/dist/src/services/harness-flywheel.d.ts +2 -0
- package/dist/src/services/harness-flywheel.js +1 -0
- package/dist/src/services/harness-project-anchor.d.ts +23 -0
- package/dist/src/services/harness-project-anchor.js +129 -0
- package/dist/src/services/learned-routing.d.ts +34 -0
- package/dist/src/services/learned-routing.js +85 -0
- package/dist/src/services/memory-distillation.d.ts +1 -0
- package/dist/src/services/memory-distillation.js +81 -5
- package/dist/src/services/pheromone-adaptive.d.ts +71 -0
- package/dist/src/services/pheromone-adaptive.js +214 -0
- package/dist/src/services/worker-daemon.js +5 -1
- package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
- package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.js +9 -13
- package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
- package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
- package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
- package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
- package/plugins/ruflo-metaharness/scripts/threat-model.mjs +4 -1
|
@@ -8,6 +8,7 @@ import { dirname, join, resolve } from 'path';
|
|
|
8
8
|
import { getProjectCwd } from './types.js';
|
|
9
9
|
import { validateIdentifier, validateText, validatePath } from './validate-input.js';
|
|
10
10
|
import { checkCommandLoop, recordCommandOutcome } from './tool-loop-guardrail.js';
|
|
11
|
+
import { buildLearnedRoutingPatterns, } from '../services/learned-routing.js';
|
|
11
12
|
// Real vector search functions - lazy loaded to avoid circular imports
|
|
12
13
|
let searchEntriesFn = null;
|
|
13
14
|
/**
|
|
@@ -185,6 +186,13 @@ function saveRoutingOutcomes(outcomes) {
|
|
|
185
186
|
// Cap at 500 entries to bound file size
|
|
186
187
|
const capped = outcomes.slice(-500);
|
|
187
188
|
writeFileSync(ROUTING_OUTCOMES_PATH, JSON.stringify({ outcomes: capped }, null, 2));
|
|
189
|
+
// A long-lived MCP process must observe the new label on the next route.
|
|
190
|
+
// The prior singleton cache made the learned store inert until restart.
|
|
191
|
+
semanticRouter = null;
|
|
192
|
+
nativeVectorDb = null;
|
|
193
|
+
semanticRouterInitialized = false;
|
|
194
|
+
routerBackend = 'none';
|
|
195
|
+
TASK_PATTERN_EMBEDDINGS.clear();
|
|
188
196
|
}
|
|
189
197
|
catch { /* non-critical */ }
|
|
190
198
|
}
|
|
@@ -194,24 +202,7 @@ function saveRoutingOutcomes(outcomes) {
|
|
|
194
202
|
* merged into both the native HNSW and pure-JS semantic routers.
|
|
195
203
|
*/
|
|
196
204
|
function loadLearnedPatterns() {
|
|
197
|
-
|
|
198
|
-
const byAgent = {};
|
|
199
|
-
for (const o of outcomes) {
|
|
200
|
-
if (!o.success || !o.agent || !o.keywords?.length)
|
|
201
|
-
continue;
|
|
202
|
-
if (!byAgent[o.agent])
|
|
203
|
-
byAgent[o.agent] = new Set();
|
|
204
|
-
for (const kw of o.keywords)
|
|
205
|
-
byAgent[o.agent].add(kw);
|
|
206
|
-
}
|
|
207
|
-
const patterns = {};
|
|
208
|
-
for (const [agent, kwSet] of Object.entries(byAgent)) {
|
|
209
|
-
patterns[`learned-${agent}`] = {
|
|
210
|
-
keywords: [...kwSet].slice(0, 50),
|
|
211
|
-
agents: [agent],
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
return patterns;
|
|
205
|
+
return buildLearnedRoutingPatterns(loadRoutingOutcomes());
|
|
215
206
|
}
|
|
216
207
|
/**
|
|
217
208
|
* Merge static TASK_PATTERNS with runtime-learned patterns.
|
|
@@ -290,46 +281,55 @@ async function getSemanticRouter() {
|
|
|
290
281
|
// STEP 1: Try native VectorDb from @ruvector/router (HNSW-backed)
|
|
291
282
|
// Note: Native VectorDb uses a persistent database file which can have lock issues
|
|
292
283
|
// in concurrent environments. We try it first but fall back gracefully to pure JS.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
284
|
+
// Deterministic/test and lock-constrained environments may force the
|
|
285
|
+
// pure-JS router; production continues to prefer the native backend.
|
|
286
|
+
if (process.env.CLAUDE_FLOW_DISABLE_NATIVE_ROUTER !== '1')
|
|
287
|
+
try {
|
|
288
|
+
// Use createRequire for ESM compatibility with native modules
|
|
289
|
+
const { createRequire } = await import('module');
|
|
290
|
+
const require = createRequire(import.meta.url);
|
|
291
|
+
const router = require('@ruvector/router');
|
|
292
|
+
if (router.VectorDb && router.DistanceMetric) {
|
|
293
|
+
// Try to create VectorDb - may fail with lock error in concurrent envs
|
|
294
|
+
const db = new router.VectorDb({
|
|
295
|
+
dimensions: 384,
|
|
296
|
+
distanceMetric: router.DistanceMetric.Cosine,
|
|
297
|
+
hnswM: 16,
|
|
298
|
+
hnswEfConstruction: 200,
|
|
299
|
+
hnswEfSearch: 100,
|
|
300
|
+
});
|
|
301
|
+
// Initialize with static + runtime-learned task patterns
|
|
302
|
+
for (const [patternName, { keywords }] of Object.entries(getMergedTaskPatterns())) {
|
|
303
|
+
for (const keyword of keywords) {
|
|
304
|
+
const embedding = generateSimpleEmbedding(keyword);
|
|
305
|
+
db.insert(`${patternName}:${keyword}`, embedding);
|
|
306
|
+
TASK_PATTERN_EMBEDDINGS.set(`${patternName}:${keyword}`, embedding);
|
|
307
|
+
}
|
|
313
308
|
}
|
|
309
|
+
nativeVectorDb = db;
|
|
310
|
+
routerBackend = 'native';
|
|
311
|
+
console.log('[hooks] Semantic router initialized: native VectorDb (HNSW, 16k+ routes/s)');
|
|
312
|
+
return { router: null, backend: routerBackend, native: nativeVectorDb };
|
|
314
313
|
}
|
|
315
|
-
nativeVectorDb = db;
|
|
316
|
-
routerBackend = 'native';
|
|
317
|
-
console.log('[hooks] Semantic router initialized: native VectorDb (HNSW, 16k+ routes/s)');
|
|
318
|
-
return { router: null, backend: routerBackend, native: nativeVectorDb };
|
|
319
314
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
// Native not available or database locked - fall back to pure JS
|
|
317
|
+
// Common errors: "Database already open. Cannot acquire lock." or "MODULE_NOT_FOUND"
|
|
318
|
+
// This is expected in concurrent environments or when binary isn't installed
|
|
319
|
+
}
|
|
326
320
|
// STEP 2: Fall back to pure JS SemanticRouter
|
|
327
321
|
try {
|
|
328
322
|
const { SemanticRouter } = await import('../ruvector/semantic-router.js');
|
|
329
323
|
semanticRouter = new SemanticRouter({ dimension: 384 });
|
|
330
|
-
for (const [patternName, { keywords, agents }] of Object.entries(getMergedTaskPatterns())) {
|
|
324
|
+
for (const [patternName, { keywords, agents, source, support, reliability }] of Object.entries(getMergedTaskPatterns())) {
|
|
331
325
|
const embeddings = keywords.map(kw => generateSimpleEmbedding(kw));
|
|
332
|
-
semanticRouter.addIntentWithEmbeddings(patternName, embeddings, {
|
|
326
|
+
semanticRouter.addIntentWithEmbeddings(patternName, embeddings, {
|
|
327
|
+
agents,
|
|
328
|
+
keywords,
|
|
329
|
+
source: source ?? 'static',
|
|
330
|
+
support,
|
|
331
|
+
reliability,
|
|
332
|
+
});
|
|
333
333
|
// Cache embeddings for keywords
|
|
334
334
|
keywords.forEach((kw, i) => {
|
|
335
335
|
TASK_PATTERN_EMBEDDINGS.set(kw, embeddings[i]);
|
|
@@ -439,6 +439,20 @@ function getIntelligenceStatsFromMemory() {
|
|
|
439
439
|
const category = e.metadata?.category || 'general';
|
|
440
440
|
categories[category] = (categories[category] || 0) + 1;
|
|
441
441
|
});
|
|
442
|
+
const successfulPatterns = patternEntries.filter(e => e.metadata?.success === true).length;
|
|
443
|
+
const failedPatterns = patternEntries.filter(e => e.metadata?.success === false).length;
|
|
444
|
+
const describedPatterns = patternEntries.filter(e => {
|
|
445
|
+
if (typeof e.metadata?.task === 'string' && e.metadata.task.trim())
|
|
446
|
+
return true;
|
|
447
|
+
try {
|
|
448
|
+
const value = typeof e.value === 'string' ? JSON.parse(e.value) : e.value;
|
|
449
|
+
return typeof value?.task === 'string' &&
|
|
450
|
+
Boolean(String(value.task).trim());
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}).length;
|
|
442
456
|
// Count routing decisions
|
|
443
457
|
const routingEntries = entries.filter(e => e.key.includes('routing') ||
|
|
444
458
|
e.metadata?.type === 'routing-decision');
|
|
@@ -472,6 +486,10 @@ function getIntelligenceStatsFromMemory() {
|
|
|
472
486
|
},
|
|
473
487
|
patterns: {
|
|
474
488
|
learned: patternEntries.length,
|
|
489
|
+
successful: successfulPatterns,
|
|
490
|
+
failed: failedPatterns,
|
|
491
|
+
unknown: Math.max(0, patternEntries.length - successfulPatterns - failedPatterns),
|
|
492
|
+
described: describedPatterns,
|
|
475
493
|
categories,
|
|
476
494
|
},
|
|
477
495
|
memory: {
|
|
@@ -946,6 +964,9 @@ export const hooksRoute = {
|
|
|
946
964
|
score: 1 - r.score, // Native uses distance (lower is better), convert to similarity
|
|
947
965
|
metadata: {
|
|
948
966
|
agents: pattern?.agents || (patternName.startsWith('learned-') ? [patternName.slice(8)] : ['coder']),
|
|
967
|
+
source: pattern?.source ?? (patternName.startsWith('learned-') ? 'learned' : 'static'),
|
|
968
|
+
support: pattern?.support,
|
|
969
|
+
reliability: pattern?.reliability,
|
|
949
970
|
},
|
|
950
971
|
};
|
|
951
972
|
});
|
|
@@ -966,8 +987,18 @@ export const hooksRoute = {
|
|
|
966
987
|
let agents;
|
|
967
988
|
let confidence;
|
|
968
989
|
let matchedPattern = '';
|
|
969
|
-
|
|
970
|
-
|
|
990
|
+
const eligibleSemantic = semanticResult.find((match) => {
|
|
991
|
+
if (match.score <= 0.4)
|
|
992
|
+
return false;
|
|
993
|
+
const learned = match.intent.startsWith('learned-') || match.metadata.source === 'learned';
|
|
994
|
+
if (!learned)
|
|
995
|
+
return true;
|
|
996
|
+
return match.score >= 0.65
|
|
997
|
+
&& Number(match.metadata.support ?? 0) >= 2
|
|
998
|
+
&& Number(match.metadata.reliability ?? 0) >= 0.75;
|
|
999
|
+
});
|
|
1000
|
+
if (eligibleSemantic) {
|
|
1001
|
+
const topMatch = eligibleSemantic;
|
|
971
1002
|
agents = topMatch.metadata.agents || ['coder', 'researcher'];
|
|
972
1003
|
confidence = topMatch.score;
|
|
973
1004
|
matchedPattern = topMatch.intent;
|
|
@@ -1061,21 +1092,28 @@ export const hooksMetrics = {
|
|
|
1061
1092
|
agentCounts[o.agent] = (agentCounts[o.agent] || 0) + 1;
|
|
1062
1093
|
}
|
|
1063
1094
|
const topAgent = Object.entries(agentCounts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
|
|
1064
|
-
const successful = stats.trajectories.successful;
|
|
1065
|
-
const total = stats.trajectories.total;
|
|
1066
|
-
const failed = Math.max(0, total - successful);
|
|
1067
1095
|
return {
|
|
1068
1096
|
_real: true,
|
|
1069
1097
|
_dataSource: 'intelligence-stats + routing-outcomes',
|
|
1070
1098
|
period,
|
|
1071
1099
|
patterns: {
|
|
1072
1100
|
total: stats.patterns.learned,
|
|
1073
|
-
successful,
|
|
1074
|
-
failed,
|
|
1101
|
+
successful: stats.patterns.successful,
|
|
1102
|
+
failed: stats.patterns.failed,
|
|
1103
|
+
unknown: stats.patterns.unknown,
|
|
1104
|
+
described: stats.patterns.described,
|
|
1105
|
+
descriptionCoverage: stats.patterns.learned > 0
|
|
1106
|
+
? stats.patterns.described / stats.patterns.learned
|
|
1107
|
+
: null,
|
|
1075
1108
|
avgConfidence: stats.routing.avgConfidence || null,
|
|
1076
1109
|
},
|
|
1077
1110
|
agents: {
|
|
1078
|
-
|
|
1111
|
+
// Confidence is a model self-score, not observed routing accuracy
|
|
1112
|
+
// (#2809). Keep the old key as an explicit null for compatibility
|
|
1113
|
+
// while exposing the truthful additive field.
|
|
1114
|
+
routingAccuracy: null,
|
|
1115
|
+
averageConfidence: stats.routing.avgConfidence || null,
|
|
1116
|
+
outcomeSuccessRate: successRate,
|
|
1079
1117
|
totalRoutes: stats.routing.decisions,
|
|
1080
1118
|
topAgent,
|
|
1081
1119
|
},
|
|
@@ -1084,7 +1122,7 @@ export const hooksMetrics = {
|
|
|
1084
1122
|
successRate,
|
|
1085
1123
|
avgRiskScore: null,
|
|
1086
1124
|
},
|
|
1087
|
-
_note:
|
|
1125
|
+
_note: stats.patterns.learned === 0 && totalCommands === 0
|
|
1088
1126
|
? 'No metrics data collected yet. Run hooks_post-task / hooks_intelligence_trajectory-end / hooks_route to populate.'
|
|
1089
1127
|
: undefined,
|
|
1090
1128
|
lastUpdated: new Date().toISOString(),
|
|
@@ -1249,6 +1287,10 @@ export const hooksPostTask = {
|
|
|
1249
1287
|
agent: { type: 'string', description: 'Agent that completed the task' },
|
|
1250
1288
|
quality: { type: 'number', description: 'Quality score (0-1)' },
|
|
1251
1289
|
task: { type: 'string', description: 'Task description text (used for learning keyword extraction)' },
|
|
1290
|
+
duration: { type: 'number', description: 'Observed task duration in milliseconds (used by pheromone-adaptive topology)' },
|
|
1291
|
+
latencyBudgetMs: { type: 'number', description: 'Latency budget used to normalize duration (default 60000ms)' },
|
|
1292
|
+
consensusAlignment: { type: 'number', description: 'Agreement with accepted swarm consensus in [0,1] (default quality)' },
|
|
1293
|
+
agentRole: { type: 'string', description: 'Role for role-local pheromone normalization (default agent)' },
|
|
1252
1294
|
storeDecisions: { type: 'boolean', description: 'Also store routing decision in memory DB' },
|
|
1253
1295
|
// ADR-147 P2: nested-subagent spawn-tree capture
|
|
1254
1296
|
parentAgentId: { type: 'string', description: 'ID of the parent agent (from Claude Code\'s parent_agent_id OTel span tag / x-claude-code-parent-agent-id header). Omit for top-level work.' },
|
|
@@ -1294,6 +1336,7 @@ export const hooksPostTask = {
|
|
|
1294
1336
|
const bridge = await import('../memory/memory-bridge.js');
|
|
1295
1337
|
feedbackResult = await bridge.bridgeRecordFeedback({
|
|
1296
1338
|
taskId,
|
|
1339
|
+
task: params.task || undefined,
|
|
1297
1340
|
success,
|
|
1298
1341
|
quality,
|
|
1299
1342
|
agent,
|
|
@@ -1405,6 +1448,22 @@ export const hooksPostTask = {
|
|
|
1405
1448
|
}
|
|
1406
1449
|
catch { /* non-critical */ }
|
|
1407
1450
|
}
|
|
1451
|
+
let pheromone;
|
|
1452
|
+
if (agent) {
|
|
1453
|
+
try {
|
|
1454
|
+
const { recordSwarmPheromoneSignal } = await import('./swarm-tools.js');
|
|
1455
|
+
const observedDuration = Math.max(0, Number(params.duration ?? (Date.now() - startTime)));
|
|
1456
|
+
const latencyBudgetMs = Math.max(1, Number(params.latencyBudgetMs ?? 60_000));
|
|
1457
|
+
pheromone = recordSwarmPheromoneSignal({
|
|
1458
|
+
agentId: agent,
|
|
1459
|
+
role: String(params.agentRole ?? agent),
|
|
1460
|
+
taskSuccess: success ? 1 : 0,
|
|
1461
|
+
normalizedLatency: Math.max(0, Math.min(1, observedDuration / latencyBudgetMs)),
|
|
1462
|
+
consensusAlignment: Math.max(0, Math.min(1, Number(params.consensusAlignment ?? quality))),
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
catch { /* no active APSC swarm or optional path unavailable */ }
|
|
1466
|
+
}
|
|
1408
1467
|
const duration = Date.now() - startTime;
|
|
1409
1468
|
// Persist to auto-memory-store for statusline visibility
|
|
1410
1469
|
try {
|
|
@@ -1444,6 +1503,7 @@ export const hooksPostTask = {
|
|
|
1444
1503
|
outcomePersisted,
|
|
1445
1504
|
},
|
|
1446
1505
|
quality,
|
|
1506
|
+
pheromone,
|
|
1447
1507
|
feedback: feedbackResult ? {
|
|
1448
1508
|
recorded: feedbackResult.success,
|
|
1449
1509
|
controller: feedbackResult.controller,
|
|
@@ -3102,7 +3162,7 @@ export const hooksIntelligenceStats = {
|
|
|
3102
3162
|
catch {
|
|
3103
3163
|
memoryStats = {
|
|
3104
3164
|
trajectories: { total: 0, successful: 0 },
|
|
3105
|
-
patterns: { learned: 0, categories: {} },
|
|
3165
|
+
patterns: { learned: 0, successful: 0, failed: 0, unknown: 0, described: 0, categories: {} },
|
|
3106
3166
|
memory: { indexSize: 0, totalAccessCount: 0, memorySizeBytes: 0 },
|
|
3107
3167
|
routing: { decisions: 0, avgConfidence: 0 },
|
|
3108
3168
|
};
|
|
@@ -1175,6 +1175,7 @@ export const memoryTools = [
|
|
|
1175
1175
|
handler: async (input) => {
|
|
1176
1176
|
await ensureInitialized();
|
|
1177
1177
|
const { listEntries, deleteEntry } = await getMemoryFunctions();
|
|
1178
|
+
const startedAt = Date.now();
|
|
1178
1179
|
const dryRun = input.dryRun !== false; // default true
|
|
1179
1180
|
const namespace = input.namespace ? String(input.namespace) : undefined;
|
|
1180
1181
|
if (namespace) {
|
|
@@ -1193,6 +1194,7 @@ export const memoryTools = [
|
|
|
1193
1194
|
});
|
|
1194
1195
|
let freedBytes = 0;
|
|
1195
1196
|
let deleted = 0;
|
|
1197
|
+
let vectorsRemoved = 0;
|
|
1196
1198
|
if (!dryRun) {
|
|
1197
1199
|
for (const e of expired) {
|
|
1198
1200
|
try {
|
|
@@ -1206,11 +1208,21 @@ export const memoryTools = [
|
|
|
1206
1208
|
else {
|
|
1207
1209
|
freedBytes = expired.reduce((s, e) => s + (e.size || 0), 0);
|
|
1208
1210
|
}
|
|
1211
|
+
if (!dryRun) {
|
|
1212
|
+
const { reconcileHNSWIndex } = await import('../memory/memory-initializer.js');
|
|
1213
|
+
vectorsRemoved = await reconcileHNSWIndex();
|
|
1214
|
+
}
|
|
1215
|
+
const formatted = freedBytes < 1024
|
|
1216
|
+
? `${freedBytes} B`
|
|
1217
|
+
: freedBytes < 1024 * 1024
|
|
1218
|
+
? `${(freedBytes / 1024).toFixed(1)} KiB`
|
|
1219
|
+
: `${(freedBytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
1209
1220
|
return {
|
|
1210
1221
|
dryRun,
|
|
1211
1222
|
candidates: { expired: expired.length, stale: 0, lowQuality: 0, total: expired.length },
|
|
1212
|
-
deleted: { entries: dryRun ? 0 : deleted, vectors:
|
|
1213
|
-
freed: { bytes: freedBytes },
|
|
1223
|
+
deleted: { entries: dryRun ? 0 : deleted, vectors: vectorsRemoved, patterns: 0 },
|
|
1224
|
+
freed: { bytes: freedBytes, formatted },
|
|
1225
|
+
duration: Date.now() - startedAt,
|
|
1214
1226
|
note: dryRun ? 'dry run — re-run with dryRun:false to delete' : undefined,
|
|
1215
1227
|
};
|
|
1216
1228
|
},
|
|
@@ -700,6 +700,9 @@ export const metaharnessTools = [
|
|
|
700
700
|
confirm: { type: 'boolean', description: 'Required for promote; never inferred', default: false },
|
|
701
701
|
maxConcurrency: { type: 'number', description: 'ADR-324 hard local candidate-evaluation concurrency cap (1-8)', default: 2 },
|
|
702
702
|
timeoutMs: { type: 'number', description: 'Abort concurrent evaluation after this wall-clock limit', default: 120000 },
|
|
703
|
+
anchorPath: { type: 'string', description: 'Project-contained human-labelled anchor JSON; requires anchorHash' },
|
|
704
|
+
anchorHash: { type: 'string', description: 'Pinned sha256 of canonical anchor tasks; requires anchorPath' },
|
|
705
|
+
anchorManifestPath: { type: 'string', description: 'Project-contained anchor manifest path (default .claude/eval/flywheel-anchor.manifest.json)' },
|
|
703
706
|
approvalIds: { type: 'array', items: { type: 'string' }, description: 'Scoped ADR-324 approval IDs for privileged promotion' },
|
|
704
707
|
},
|
|
705
708
|
required: ['operation'],
|
|
@@ -721,6 +724,7 @@ export const metaharnessTools = [
|
|
|
721
724
|
if (operation === 'receipts') {
|
|
722
725
|
const data = listFlywheelReceipts(projectRoot).map(({ receipt, state }) => ({
|
|
723
726
|
receiptId: receipt.payload.receiptId,
|
|
727
|
+
anchorRef: receipt.payload.anchorRef,
|
|
724
728
|
candidateId: receipt.payload.candidateId,
|
|
725
729
|
baselineRef: receipt.payload.baselineRef,
|
|
726
730
|
decision: receipt.payload.decision,
|
|
@@ -748,6 +752,9 @@ export const metaharnessTools = [
|
|
|
748
752
|
receiptPublicKeyPem: publicKeyPath ? readFileSync(publicKeyPath, 'utf8') : undefined,
|
|
749
753
|
maxConcurrency: Number(input.maxConcurrency ?? 2),
|
|
750
754
|
evaluationTimeoutMs: Number(input.timeoutMs ?? 120_000),
|
|
755
|
+
anchorPath: input.anchorPath ? String(input.anchorPath) : undefined,
|
|
756
|
+
anchorHash: input.anchorHash ? String(input.anchorHash) : undefined,
|
|
757
|
+
anchorManifestPath: input.anchorManifestPath ? String(input.anchorManifestPath) : undefined,
|
|
751
758
|
});
|
|
752
759
|
return { success: data.ran, data, degraded: false, exitCode: data.ran ? 0 : 1 };
|
|
753
760
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Replaces previous stub implementations with real state tracking.
|
|
6
6
|
*/
|
|
7
7
|
import { type MCPTool } from './types.js';
|
|
8
|
+
import { type ApscDecision, type ApscSignal } from '../services/pheromone-adaptive.js';
|
|
8
9
|
interface SwarmState {
|
|
9
10
|
swarmId: string;
|
|
10
11
|
topology: string;
|
|
@@ -32,6 +33,21 @@ interface SwarmStore {
|
|
|
32
33
|
}
|
|
33
34
|
export declare function loadSwarmStore(): SwarmStore;
|
|
34
35
|
export declare function saveSwarmStore(store: SwarmStore): void;
|
|
36
|
+
/** Shared by hooks_post-task so outcome labels immediately drive APSC. */
|
|
37
|
+
export declare function recordSwarmPheromoneSignal(signal: ApscSignal): {
|
|
38
|
+
active: false;
|
|
39
|
+
reason: string;
|
|
40
|
+
} | {
|
|
41
|
+
active: true;
|
|
42
|
+
swarmId: string;
|
|
43
|
+
decision: ApscDecision;
|
|
44
|
+
};
|
|
45
|
+
/** Scheduling gate used by agent_execute. Dry-run topologies remain eligible. */
|
|
46
|
+
export declare function pheromoneAgentEligibility(agentId: string): {
|
|
47
|
+
eligible: boolean;
|
|
48
|
+
active: boolean;
|
|
49
|
+
reason?: string;
|
|
50
|
+
};
|
|
35
51
|
export declare const swarmTools: MCPTool[];
|
|
36
52
|
export {};
|
|
37
53
|
//# sourceMappingURL=swarm-tools.d.ts.map
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* Tool definitions for swarm coordination with file-based state persistence.
|
|
5
5
|
* Replaces previous stub implementations with real state tracking.
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { getProjectCwd } from './types.js';
|
|
10
10
|
import { validateIdentifier } from './validate-input.js';
|
|
11
|
+
import { createApscState, isApscAgentEligible, normalizeApscConfig, recordApscSignal, } from '../services/pheromone-adaptive.js';
|
|
11
12
|
// Swarm state persistence
|
|
12
13
|
const SWARM_DIR = '.claude-flow/swarm';
|
|
13
14
|
const SWARM_STATE_FILE = 'swarm-state.json';
|
|
15
|
+
const SWARM_STATE_LOCK = 'swarm-state.lock';
|
|
16
|
+
const SWARM_LOCK_STALE_MS = 10_000;
|
|
14
17
|
function getSwarmDir() {
|
|
15
18
|
return join(getProjectCwd(), SWARM_DIR);
|
|
16
19
|
}
|
|
@@ -103,12 +106,101 @@ export function loadSwarmStore() {
|
|
|
103
106
|
}
|
|
104
107
|
export function saveSwarmStore(store) {
|
|
105
108
|
ensureSwarmDir();
|
|
106
|
-
|
|
109
|
+
const target = getSwarmStatePath();
|
|
110
|
+
const temporary = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
111
|
+
try {
|
|
112
|
+
writeFileSync(temporary, JSON.stringify(store, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
113
|
+
renameSync(temporary, target);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
try {
|
|
117
|
+
unlinkSync(temporary);
|
|
118
|
+
}
|
|
119
|
+
catch { /* already renamed or never created */ }
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function withSwarmStoreLock(operation) {
|
|
124
|
+
ensureSwarmDir();
|
|
125
|
+
const lockPath = join(getSwarmDir(), SWARM_STATE_LOCK);
|
|
126
|
+
let descriptor;
|
|
127
|
+
const waiter = new Int32Array(new SharedArrayBuffer(4));
|
|
128
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
129
|
+
try {
|
|
130
|
+
descriptor = openSync(lockPath, 'wx', 0o600);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code !== 'EEXIST')
|
|
135
|
+
throw error;
|
|
136
|
+
try {
|
|
137
|
+
if (Date.now() - statSync(lockPath).mtimeMs > SWARM_LOCK_STALE_MS) {
|
|
138
|
+
unlinkSync(lockPath);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch { /* another writer released it */ }
|
|
143
|
+
Atomics.wait(waiter, 0, 0, 10);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (descriptor === undefined)
|
|
147
|
+
throw new Error('swarm state is busy; retry the outcome update');
|
|
148
|
+
try {
|
|
149
|
+
return operation();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
try {
|
|
153
|
+
closeSync(descriptor);
|
|
154
|
+
}
|
|
155
|
+
catch { /* best effort */ }
|
|
156
|
+
try {
|
|
157
|
+
unlinkSync(lockPath);
|
|
158
|
+
}
|
|
159
|
+
catch { /* best effort */ }
|
|
160
|
+
}
|
|
107
161
|
}
|
|
108
162
|
// Input validation
|
|
109
163
|
const VALID_TOPOLOGIES = new Set([
|
|
110
|
-
'hierarchical', 'mesh', 'hierarchical-mesh', 'ring', 'star', 'hybrid', 'adaptive',
|
|
164
|
+
'hierarchical', 'mesh', 'hierarchical-mesh', 'ring', 'star', 'hybrid', 'adaptive', 'pheromone-adaptive',
|
|
111
165
|
]);
|
|
166
|
+
function latestRunningSwarm(store) {
|
|
167
|
+
return Object.values(store.swarms)
|
|
168
|
+
.filter((swarm) => swarm.status === 'running')
|
|
169
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0];
|
|
170
|
+
}
|
|
171
|
+
function apscStateOf(swarm) {
|
|
172
|
+
if (!swarm || swarm.topology !== 'pheromone-adaptive')
|
|
173
|
+
return undefined;
|
|
174
|
+
const state = swarm.config.apscState;
|
|
175
|
+
return state?.schemaVersion ? state : undefined;
|
|
176
|
+
}
|
|
177
|
+
/** Shared by hooks_post-task so outcome labels immediately drive APSC. */
|
|
178
|
+
export function recordSwarmPheromoneSignal(signal) {
|
|
179
|
+
return withSwarmStoreLock(() => {
|
|
180
|
+
const store = loadSwarmStore();
|
|
181
|
+
const swarm = latestRunningSwarm(store);
|
|
182
|
+
const current = apscStateOf(swarm);
|
|
183
|
+
if (!swarm || !current)
|
|
184
|
+
return { active: false, reason: 'no running pheromone-adaptive swarm' };
|
|
185
|
+
const result = recordApscSignal(current, signal);
|
|
186
|
+
swarm.config.apscState = result.state;
|
|
187
|
+
swarm.updatedAt = new Date().toISOString();
|
|
188
|
+
saveSwarmStore(store);
|
|
189
|
+
return { active: true, swarmId: swarm.swarmId, decision: result.decision };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** Scheduling gate used by agent_execute. Dry-run topologies remain eligible. */
|
|
193
|
+
export function pheromoneAgentEligibility(agentId) {
|
|
194
|
+
const current = apscStateOf(latestRunningSwarm(loadSwarmStore()));
|
|
195
|
+
if (!current)
|
|
196
|
+
return { eligible: true, active: false };
|
|
197
|
+
const eligible = isApscAgentEligible(current, agentId);
|
|
198
|
+
return {
|
|
199
|
+
eligible,
|
|
200
|
+
active: true,
|
|
201
|
+
reason: eligible ? undefined : 'agent suspended by pheromone-adaptive scheduling gate',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
112
204
|
export const swarmTools = [
|
|
113
205
|
{
|
|
114
206
|
name: 'swarm_init',
|
|
@@ -117,7 +209,7 @@ export const swarmTools = [
|
|
|
117
209
|
inputSchema: {
|
|
118
210
|
type: 'object',
|
|
119
211
|
properties: {
|
|
120
|
-
topology: { type: 'string', description: 'Swarm topology type (hierarchical, mesh, hierarchical-mesh, ring, star, hybrid, adaptive)' },
|
|
212
|
+
topology: { type: 'string', description: 'Swarm topology type (hierarchical, mesh, hierarchical-mesh, ring, star, hybrid, adaptive, pheromone-adaptive)' },
|
|
121
213
|
maxAgents: { type: 'number', description: 'Maximum number of agents (1-50)' },
|
|
122
214
|
strategy: { type: 'string', description: 'Agent strategy (specialized, balanced, adaptive)' },
|
|
123
215
|
config: { type: 'object', description: 'Additional swarm configuration' },
|
|
@@ -145,6 +237,19 @@ export const swarmTools = [
|
|
|
145
237
|
error: `Invalid topology: ${topology}. Valid: ${[...VALID_TOPOLOGIES].join(', ')}`,
|
|
146
238
|
};
|
|
147
239
|
}
|
|
240
|
+
let apscState;
|
|
241
|
+
if (topology === 'pheromone-adaptive') {
|
|
242
|
+
try {
|
|
243
|
+
const apscConfig = normalizeApscConfig((config.apsc ?? {}));
|
|
244
|
+
if (apscConfig.minActiveAgents > maxAgents) {
|
|
245
|
+
return { success: false, error: 'APSC minActiveAgents cannot exceed maxAgents' };
|
|
246
|
+
}
|
|
247
|
+
apscState = createApscState(apscConfig);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
return { success: false, error: `Invalid APSC config: ${error.message}` };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
148
253
|
const swarmId = `swarm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
149
254
|
const now = new Date().toISOString();
|
|
150
255
|
const swarmState = {
|
|
@@ -161,12 +266,14 @@ export const swarmTools = [
|
|
|
161
266
|
communicationProtocol: config.communicationProtocol || 'message-bus',
|
|
162
267
|
autoScaling: config.autoScaling ?? true,
|
|
163
268
|
consensusMechanism: config.consensusMechanism || 'majority',
|
|
269
|
+
...(apscState ? { apscState } : {}),
|
|
164
270
|
},
|
|
165
271
|
createdAt: now,
|
|
166
272
|
updatedAt: now,
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
273
|
+
// A normal CLI invocation exits immediately after creating this
|
|
274
|
+
// coordination record; its PID is therefore not the swarm lifetime.
|
|
275
|
+
// Long-lived hosts may opt into PID ownership explicitly.
|
|
276
|
+
pid: config.trackHostProcess === true ? process.pid : undefined,
|
|
170
277
|
};
|
|
171
278
|
const store = loadSwarmStore();
|
|
172
279
|
store.swarms[swarmId] = swarmState;
|
|
@@ -180,6 +287,62 @@ export const swarmTools = [
|
|
|
180
287
|
initializedAt: now,
|
|
181
288
|
config: swarmState.config,
|
|
182
289
|
persisted: true,
|
|
290
|
+
apsc: apscState ? {
|
|
291
|
+
dryRun: apscState.config.dryRun,
|
|
292
|
+
minActiveAgents: apscState.config.minActiveAgents,
|
|
293
|
+
minSamples: apscState.config.minSamples,
|
|
294
|
+
} : undefined,
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'swarm_pheromone_update',
|
|
300
|
+
description: 'Record a normalized per-agent task outcome for a running pheromone-adaptive swarm. Use when a static agent pool is wrong because repeated task evidence should update role-aware EMA fitness and produce a bounded keep/suspend/reactivate decision; native Task has no persistent swarm eligibility gate.',
|
|
301
|
+
category: 'swarm',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
agentId: { type: 'string', description: 'Stable tracked agent identifier' },
|
|
306
|
+
role: { type: 'string', description: 'Agent role used for role-local normalization' },
|
|
307
|
+
taskSuccess: { type: 'number', description: 'Observed task success in [0,1]' },
|
|
308
|
+
normalizedLatency: { type: 'number', description: 'Latency divided by the configured budget, clamped to [0,1]' },
|
|
309
|
+
consensusAlignment: { type: 'number', description: 'Agreement with the accepted consensus in [0,1]' },
|
|
310
|
+
},
|
|
311
|
+
required: ['agentId', 'role', 'taskSuccess', 'normalizedLatency', 'consensusAlignment'],
|
|
312
|
+
},
|
|
313
|
+
handler: async (input) => {
|
|
314
|
+
try {
|
|
315
|
+
return recordSwarmPheromoneSignal({
|
|
316
|
+
agentId: String(input.agentId),
|
|
317
|
+
role: String(input.role),
|
|
318
|
+
taskSuccess: Number(input.taskSuccess),
|
|
319
|
+
normalizedLatency: Number(input.normalizedLatency),
|
|
320
|
+
consensusAlignment: Number(input.consensusAlignment),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return { active: false, reason: error.message };
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: 'swarm_pheromone_status',
|
|
330
|
+
description: 'Inspect the threshold, safety configuration, per-agent EMA scores, and scheduling eligibility of the active pheromone-adaptive swarm. Use when aggregate swarm status is wrong because calibration requires the exact APSC threshold and per-agent evidence before switching from dry-run to live suspension.',
|
|
331
|
+
category: 'swarm',
|
|
332
|
+
inputSchema: { type: 'object', properties: {} },
|
|
333
|
+
handler: async () => {
|
|
334
|
+
const store = loadSwarmStore();
|
|
335
|
+
const swarm = latestRunningSwarm(store);
|
|
336
|
+
const state = apscStateOf(swarm);
|
|
337
|
+
if (!swarm || !state)
|
|
338
|
+
return { active: false, reason: 'no running pheromone-adaptive swarm' };
|
|
339
|
+
return {
|
|
340
|
+
active: true,
|
|
341
|
+
swarmId: swarm.swarmId,
|
|
342
|
+
threshold: state.threshold,
|
|
343
|
+
round: state.round,
|
|
344
|
+
config: state.config,
|
|
345
|
+
agents: Object.values(state.agents).sort((a, b) => a.agentId.localeCompare(b.agentId)),
|
|
183
346
|
};
|
|
184
347
|
},
|
|
185
348
|
},
|
|
@@ -212,6 +375,7 @@ export const swarmTools = [
|
|
|
212
375
|
agentCount: swarm.agents.length,
|
|
213
376
|
taskCount: swarm.tasks.length,
|
|
214
377
|
config: swarm.config,
|
|
378
|
+
apsc: apscStateOf(swarm),
|
|
215
379
|
createdAt: swarm.createdAt,
|
|
216
380
|
updatedAt: swarm.updatedAt,
|
|
217
381
|
};
|
|
@@ -236,6 +400,7 @@ export const swarmTools = [
|
|
|
236
400
|
agentCount: latest.agents.length,
|
|
237
401
|
taskCount: latest.tasks.length,
|
|
238
402
|
config: latest.config,
|
|
403
|
+
apsc: apscStateOf(latest),
|
|
239
404
|
createdAt: latest.createdAt,
|
|
240
405
|
updatedAt: latest.updatedAt,
|
|
241
406
|
totalSwarms: swarmIds.length,
|
|
@@ -322,6 +322,8 @@ export declare function bridgeSearchPatterns(options: {
|
|
|
322
322
|
*/
|
|
323
323
|
export declare function bridgeRecordFeedback(options: {
|
|
324
324
|
taskId: string;
|
|
325
|
+
/** Human-readable task text used as the semantic learning signal. */
|
|
326
|
+
task?: string;
|
|
325
327
|
success: boolean;
|
|
326
328
|
quality: number;
|
|
327
329
|
agent?: string;
|