@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.
Files changed (71) hide show
  1. package/.claude/helpers/helpers.manifest.json +5 -5
  2. package/.claude/helpers/hook-handler.cjs +30 -0
  3. package/.claude/helpers/intelligence.cjs +47 -4
  4. package/.claude/helpers/statusline.cjs +169 -6
  5. package/bin/cli.js +25 -1
  6. package/catalog-manifest.json +4 -4
  7. package/dist/src/commands/agent.js +50 -2
  8. package/dist/src/commands/doctor.js +159 -11
  9. package/dist/src/commands/hooks.js +63 -4
  10. package/dist/src/commands/mcp.js +1 -1
  11. package/dist/src/commands/memory-distill.js +1 -0
  12. package/dist/src/commands/metaharness.js +4 -0
  13. package/dist/src/commands/swarm.js +89 -5
  14. package/dist/src/config-adapter.js +1 -0
  15. package/dist/src/index.js +3 -2
  16. package/dist/src/init/executor.js +16 -10
  17. package/dist/src/init/helpers-generator.js +15 -4
  18. package/dist/src/init/statusline-generator.js +18 -5
  19. package/dist/src/mcp-server.d.ts +25 -0
  20. package/dist/src/mcp-server.js +57 -2
  21. package/dist/src/mcp-tools/agent-tools.js +10 -0
  22. package/dist/src/mcp-tools/embeddings-tools.js +51 -10
  23. package/dist/src/mcp-tools/hooks-tools.js +120 -60
  24. package/dist/src/mcp-tools/memory-tools.js +14 -2
  25. package/dist/src/mcp-tools/metaharness-tools.js +7 -0
  26. package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
  27. package/dist/src/mcp-tools/swarm-tools.js +172 -7
  28. package/dist/src/memory/memory-bridge.d.ts +2 -0
  29. package/dist/src/memory/memory-bridge.js +5 -1
  30. package/dist/src/memory/memory-initializer.d.ts +9 -1
  31. package/dist/src/memory/memory-initializer.js +66 -19
  32. package/dist/src/services/daemon-autostart.js +7 -4
  33. package/dist/src/services/flywheel-receipt.d.ts +3 -0
  34. package/dist/src/services/flywheel-receipt.js +1 -0
  35. package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
  36. package/dist/src/services/harness-flywheel-runtime.js +19 -19
  37. package/dist/src/services/harness-flywheel.d.ts +2 -0
  38. package/dist/src/services/harness-flywheel.js +1 -0
  39. package/dist/src/services/harness-project-anchor.d.ts +23 -0
  40. package/dist/src/services/harness-project-anchor.js +129 -0
  41. package/dist/src/services/learned-routing.d.ts +34 -0
  42. package/dist/src/services/learned-routing.js +85 -0
  43. package/dist/src/services/memory-distillation.d.ts +1 -0
  44. package/dist/src/services/memory-distillation.js +81 -5
  45. package/dist/src/services/pheromone-adaptive.d.ts +71 -0
  46. package/dist/src/services/pheromone-adaptive.js +214 -0
  47. package/dist/src/services/worker-daemon.js +5 -1
  48. package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
  49. package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
  50. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
  51. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
  52. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
  53. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
  54. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
  55. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  56. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
  57. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  58. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
  59. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
  60. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
  61. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
  62. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  63. package/node_modules/@claude-flow/codex/dist/initializer.js +9 -13
  64. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  65. package/package.json +1 -1
  66. package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
  67. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
  68. package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
  69. package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
  70. package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
  71. 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
- const outcomes = loadRoutingOutcomes();
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
- try {
294
- // Use createRequire for ESM compatibility with native modules
295
- const { createRequire } = await import('module');
296
- const require = createRequire(import.meta.url);
297
- const router = require('@ruvector/router');
298
- if (router.VectorDb && router.DistanceMetric) {
299
- // Try to create VectorDb - may fail with lock error in concurrent envs
300
- const db = new router.VectorDb({
301
- dimensions: 384,
302
- distanceMetric: router.DistanceMetric.Cosine,
303
- hnswM: 16,
304
- hnswEfConstruction: 200,
305
- hnswEfSearch: 100,
306
- });
307
- // Initialize with static + runtime-learned task patterns
308
- for (const [patternName, { keywords }] of Object.entries(getMergedTaskPatterns())) {
309
- for (const keyword of keywords) {
310
- const embedding = generateSimpleEmbedding(keyword);
311
- db.insert(`${patternName}:${keyword}`, embedding);
312
- TASK_PATTERN_EMBEDDINGS.set(`${patternName}:${keyword}`, embedding);
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
- catch (err) {
322
- // Native not available or database locked - fall back to pure JS
323
- // Common errors: "Database already open. Cannot acquire lock." or "MODULE_NOT_FOUND"
324
- // This is expected in concurrent environments or when binary isn't installed
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, { agents, keywords });
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
- if (semanticResult.length > 0 && semanticResult[0].score > 0.4) {
970
- const topMatch = semanticResult[0];
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
- routingAccuracy: stats.routing.avgConfidence || null,
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: total === 0 && totalCommands === 0
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: 0, patterns: 0 },
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
- writeFileSync(getSwarmStatePath(), JSON.stringify(store, null, 2), 'utf-8');
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
- // #1799 record host PID so subsequent loads can detect orphans
168
- // when this process exits without a graceful swarm_shutdown.
169
- pid: process.pid,
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;