@claude-flow/cli 3.32.34 → 3.32.35

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 (31) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/catalog-manifest.json +4 -4
  3. package/dist/src/commands/agent.js +49 -1
  4. package/dist/src/commands/hooks.js +24 -0
  5. package/dist/src/commands/metaharness.js +4 -0
  6. package/dist/src/commands/swarm.js +61 -5
  7. package/dist/src/config-adapter.js +1 -0
  8. package/dist/src/index.js +3 -2
  9. package/dist/src/mcp-tools/agent-tools.js +10 -0
  10. package/dist/src/mcp-tools/hooks-tools.js +86 -52
  11. package/dist/src/mcp-tools/memory-tools.js +14 -2
  12. package/dist/src/mcp-tools/metaharness-tools.js +7 -0
  13. package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
  14. package/dist/src/mcp-tools/swarm-tools.js +172 -7
  15. package/dist/src/memory/memory-initializer.d.ts +9 -1
  16. package/dist/src/memory/memory-initializer.js +66 -19
  17. package/dist/src/services/daemon-autostart.js +7 -4
  18. package/dist/src/services/flywheel-receipt.d.ts +3 -0
  19. package/dist/src/services/flywheel-receipt.js +1 -0
  20. package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
  21. package/dist/src/services/harness-flywheel-runtime.js +19 -19
  22. package/dist/src/services/harness-flywheel.d.ts +2 -0
  23. package/dist/src/services/harness-flywheel.js +1 -0
  24. package/dist/src/services/harness-project-anchor.d.ts +23 -0
  25. package/dist/src/services/harness-project-anchor.js +129 -0
  26. package/dist/src/services/learned-routing.d.ts +34 -0
  27. package/dist/src/services/learned-routing.js +85 -0
  28. package/dist/src/services/pheromone-adaptive.d.ts +71 -0
  29. package/dist/src/services/pheromone-adaptive.js +214 -0
  30. package/dist/src/services/worker-daemon.js +4 -1
  31. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.32.34",
3
+ "version": "3.32.35",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "68be7e9a9eba7bf9c4e8a230db7bf61a243b965639f8504842799d6c6ca28762",
6
6
  "hook-handler.cjs": "50ea92a72651bdc95634f7588d56a5870963168eef5226f66ed14af3b47c8d9a",
@@ -8,6 +8,6 @@
8
8
  "statusline.cjs": "d2a0eac56d1267d8dbed2d70b09feb592299469196ec4b215909f2b052144882"
9
9
  }
10
10
  },
11
- "signature": "i0qilACTabRlI6VORA0QyP0r4EuvJsrvtkucxia3TStgq/la250iesyKjlcHbLV/6fshZiGtPMLq1nmXLf+ZAw==",
11
+ "signature": "7hAABpKaZablL78ug8UW506P+ppwYlDb2qws0D4BbCeugfGQTdDEOYsndbZMc4+Wzbx10oQbnubqh8jHZnHvBQ==",
12
12
  "algorithm": "ed25519"
13
13
  }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generation": 3,
4
- "generatedAt": "2026-07-29T13:29:39.000Z",
5
- "gitSha": "c3cf2d0e",
3
+ "generation": 4,
4
+ "generatedAt": "2026-07-29T17:56:33.000Z",
5
+ "gitSha": "3d9bca6d",
6
6
  "catalog": {
7
7
  "agents": 164,
8
- "tools": 395,
8
+ "tools": 397,
9
9
  "skills": 34
10
10
  },
11
11
  "benchmark": null
@@ -487,6 +487,39 @@ const metricsCommand = {
487
487
  const avgSuccessRate = tasksCompleted > 0
488
488
  ? `${Math.round(Object.values(typeCounts).reduce((a, d) => a + d.success, 0) / tasksCompleted * 100)}%`
489
489
  : 'N/A';
490
+ // ADR-330: expose the learned scheduling signal where operators already
491
+ // inspect agent metrics. Old/non-APSC installs simply omit this section.
492
+ let pheromone;
493
+ const swarmStatePath = join(process.cwd(), '.claude-flow', 'swarm', 'swarm-state.json');
494
+ if (existsSync(swarmStatePath)) {
495
+ try {
496
+ const store = JSON.parse(readFileSync(swarmStatePath, 'utf-8'));
497
+ const current = Object.values(store.swarms ?? {})
498
+ .filter((swarm) => swarm.status === 'running' && swarm.topology === 'pheromone-adaptive')
499
+ .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0];
500
+ const apsc = current?.config?.apscState;
501
+ if (apsc) {
502
+ const agents = Object.values(apsc.agents ?? {})
503
+ .filter((agent) => !agentId || agent.agentId === agentId)
504
+ .sort((a, b) => a.agentId.localeCompare(b.agentId))
505
+ .map((agent) => ({
506
+ agentId: agent.agentId,
507
+ role: agent.role,
508
+ pheromoneScore: agent.emaScore,
509
+ rawScore: agent.rawScore,
510
+ samples: agent.samples,
511
+ eligibility: agent.status === 'suspended' ? 'suspended' : 'eligible',
512
+ }));
513
+ pheromone = {
514
+ active: true,
515
+ dryRun: apsc.config?.dryRun !== false,
516
+ threshold: apsc.threshold ?? 0.5,
517
+ agents,
518
+ };
519
+ }
520
+ }
521
+ catch { /* malformed/legacy swarm state — preserve existing metrics */ }
522
+ }
490
523
  const metrics = {
491
524
  period,
492
525
  summary: {
@@ -501,7 +534,8 @@ const metricsCommand = {
501
534
  performance: {
502
535
  memoryVectors: `${vectorCount} vectors`,
503
536
  searchBackend: vectorCount > 0 ? 'HNSW-indexed' : 'none'
504
- }
537
+ },
538
+ pheromone,
505
539
  };
506
540
  if (ctx.flags.format === 'json') {
507
541
  output.printJson(metrics);
@@ -544,6 +578,20 @@ const metricsCommand = {
544
578
  `Vectors: ${output.success(metrics.performance.memoryVectors)}`,
545
579
  `Backend: ${output.success(metrics.performance.searchBackend)}`
546
580
  ]);
581
+ if (metrics.pheromone) {
582
+ output.writeln();
583
+ output.writeln(output.bold(`Pheromone Scheduling (${metrics.pheromone.dryRun ? 'dry run' : 'live'})`));
584
+ output.printTable({
585
+ columns: [
586
+ { key: 'agentId', header: 'Agent', width: 20 },
587
+ { key: 'role', header: 'Role', width: 18 },
588
+ { key: 'pheromoneScore', header: 'EMA Score', width: 12, align: 'right' },
589
+ { key: 'samples', header: 'Samples', width: 9, align: 'right' },
590
+ { key: 'eligibility', header: 'Eligibility', width: 12 },
591
+ ],
592
+ data: metrics.pheromone.agents,
593
+ });
594
+ }
547
595
  return { success: true, data: metrics };
548
596
  }
549
597
  };
@@ -1672,6 +1672,26 @@ const postTaskCommand = {
1672
1672
  description: 'Agent that executed the task',
1673
1673
  type: 'string'
1674
1674
  },
1675
+ {
1676
+ name: 'agent-role',
1677
+ description: 'Stable agent role used for role-aware pheromone comparison (for example coder, tester, coordinator)',
1678
+ type: 'string'
1679
+ },
1680
+ {
1681
+ name: 'duration',
1682
+ description: 'Observed task duration in milliseconds for latency fitness',
1683
+ type: 'number'
1684
+ },
1685
+ {
1686
+ name: 'latency-budget-ms',
1687
+ description: 'Expected task duration in milliseconds; paired with --duration to normalize latency fitness',
1688
+ type: 'number'
1689
+ },
1690
+ {
1691
+ name: 'consensus-alignment',
1692
+ description: 'Consensus alignment score from 0 to 1',
1693
+ type: 'number'
1694
+ },
1675
1695
  {
1676
1696
  name: 'task',
1677
1697
  short: 't',
@@ -1716,6 +1736,10 @@ const postTaskCommand = {
1716
1736
  success,
1717
1737
  quality: ctx.flags.quality,
1718
1738
  agent: ctx.flags.agent,
1739
+ agentRole: ctx.flags.agentRole,
1740
+ duration: ctx.flags.duration,
1741
+ latencyBudgetMs: ctx.flags.latencyBudgetMs,
1742
+ consensusAlignment: ctx.flags.consensusAlignment,
1719
1743
  // #2785: forward the task description so routing outcomes actually persist
1720
1744
  // (hooks_post-task requires taskText + agent to write the outcome row that
1721
1745
  // hooks_metrics reads via getIntelligenceStatsFromMemory)
@@ -102,12 +102,16 @@ async function dispatchFlywheel(operation, positional, flags) {
102
102
  receiptPublicKeyPem: publicKeyPath ? readFileSync(resolve(publicKeyPath), 'utf8') : undefined,
103
103
  maxConcurrency: Number(flywheelFlag(flags, 'maxConcurrency', 2)),
104
104
  evaluationTimeoutMs: Number(flywheelFlag(flags, 'timeoutMs', 120_000)),
105
+ anchorPath: flywheelFlag(flags, 'anchorPath'),
106
+ anchorHash: flywheelFlag(flags, 'anchorHash'),
107
+ anchorManifestPath: flywheelFlag(flags, 'anchorManifest'),
105
108
  });
106
109
  }
107
110
  }
108
111
  else if (operation === 'receipts') {
109
112
  data = listFlywheelReceipts(projectRoot).map(({ receipt, state }) => ({
110
113
  receiptId: receipt.payload.receiptId,
114
+ anchorRef: receipt.payload.anchorRef,
111
115
  candidateId: receipt.payload.candidateId,
112
116
  baselineRef: receipt.payload.baselineRef,
113
117
  decision: receipt.payload.decision,
@@ -271,7 +271,8 @@ const TOPOLOGIES = [
271
271
  { value: 'ring', label: 'Ring', hint: 'Circular communication pattern' },
272
272
  { value: 'star', label: 'Star', hint: 'Central coordinator with spoke agents' },
273
273
  { value: 'hybrid', label: 'Hybrid', hint: 'Hierarchical mesh for maximum flexibility' },
274
- { value: 'hierarchical-mesh', label: 'Hierarchical Mesh', hint: 'V3 15-agent queen + peer communication (recommended)' }
274
+ { value: 'hierarchical-mesh', label: 'Hierarchical Mesh', hint: 'V3 15-agent queen + peer communication (recommended)' },
275
+ { value: 'pheromone-adaptive', label: 'Pheromone Adaptive', hint: 'Role-aware dynamic eligibility with quorum safety (ADR-330)' }
275
276
  ];
276
277
  // Swarm strategies
277
278
  const STRATEGIES = [
@@ -324,6 +325,19 @@ const initCommand = {
324
325
  type: 'boolean',
325
326
  default: false
326
327
  },
328
+ {
329
+ name: 'apsc-live',
330
+ description: 'Apply APSC suspension decisions (default is calibration-only dry run)',
331
+ type: 'boolean',
332
+ default: false
333
+ },
334
+ { name: 'apsc-alpha', description: 'Task-success score weight', type: 'number', default: 0.5 },
335
+ { name: 'apsc-beta', description: 'Latency score weight', type: 'number', default: 0.2 },
336
+ { name: 'apsc-gamma', description: 'Consensus-alignment score weight', type: 'number', default: 0.3 },
337
+ { name: 'apsc-pruning-factor', description: 'Fraction of adaptive threshold below which agents are eligible for suspension', type: 'number', default: 0.6 },
338
+ { name: 'apsc-reactivation-threshold', description: 'Fraction of adaptive threshold required for recovery', type: 'number', default: 0.75 },
339
+ { name: 'apsc-min-active-agents', description: 'Hard quorum floor', type: 'number', default: 3 },
340
+ { name: 'apsc-min-samples', description: 'Warm-up observations before pruning', type: 'number', default: 3 },
327
341
  {
328
342
  // #2768 — dream-cycle SubagentPermissionDelegate. Ships a per-role
329
343
  // capability manifest to `.swarm/permissions.jsonl` + an append-only
@@ -366,6 +380,18 @@ const initCommand = {
366
380
  failureHandling: 'retry',
367
381
  loadBalancing: true,
368
382
  autoScaling: ctx.flags.autoScale ?? true,
383
+ ...(topology === 'pheromone-adaptive' ? {
384
+ apsc: {
385
+ alpha: Number(ctx.flags.apscAlpha ?? 0.5),
386
+ beta: Number(ctx.flags.apscBeta ?? 0.2),
387
+ gamma: Number(ctx.flags.apscGamma ?? 0.3),
388
+ pruningFactor: Number(ctx.flags.apscPruningFactor ?? 0.6),
389
+ reactivationThreshold: Number(ctx.flags.apscReactivationThreshold ?? 0.75),
390
+ minActiveAgents: Number(ctx.flags.apscMinActiveAgents ?? 3),
391
+ minSamples: Number(ctx.flags.apscMinSamples ?? 3),
392
+ dryRun: ctx.flags.apscLive !== true,
393
+ },
394
+ } : {}),
369
395
  },
370
396
  metadata: {
371
397
  v3Mode,
@@ -393,7 +419,10 @@ const initCommand = {
393
419
  { property: 'Max Agents', value: result.config.maxAgents },
394
420
  { property: 'Auto Scale', value: result.config.autoScaling ? 'Enabled' : 'Disabled' },
395
421
  { property: 'Protocol', value: result.config.communicationProtocol || 'N/A' },
396
- { property: 'V3 Mode', value: v3Mode ? 'Enabled' : 'Disabled' }
422
+ { property: 'V3 Mode', value: v3Mode ? 'Enabled' : 'Disabled' },
423
+ ...(topology === 'pheromone-adaptive'
424
+ ? [{ property: 'APSC Mode', value: ctx.flags.apscLive === true ? 'Live' : 'Dry run' }]
425
+ : [])
397
426
  ]
398
427
  });
399
428
  output.writeln();
@@ -924,15 +953,41 @@ const compressMessageCommand = {
924
953
  return { success: true, data: result };
925
954
  },
926
955
  };
956
+ const pheromoneCommand = {
957
+ name: 'pheromone',
958
+ description: 'Inspect or update ADR-330 pheromone-adaptive scheduling state',
959
+ options: [
960
+ { name: 'agent-id', description: 'Agent identifier; omit to show status', type: 'string' },
961
+ { name: 'role', description: 'Agent role for role-local normalization', type: 'string' },
962
+ { name: 'task-success', description: 'Task success in [0,1]', type: 'number' },
963
+ { name: 'normalized-latency', description: 'Latency divided by budget in [0,1]', type: 'number' },
964
+ { name: 'consensus-alignment', description: 'Consensus alignment in [0,1]', type: 'number' },
965
+ ],
966
+ action: async (ctx) => {
967
+ const agentId = ctx.flags.agentId;
968
+ const data = agentId
969
+ ? await callMCPTool('swarm_pheromone_update', {
970
+ agentId,
971
+ role: String(ctx.flags.role ?? 'worker'),
972
+ taskSuccess: Number(ctx.flags.taskSuccess ?? 1),
973
+ normalizedLatency: Number(ctx.flags.normalizedLatency ?? 0),
974
+ consensusAlignment: Number(ctx.flags.consensusAlignment ?? 1),
975
+ })
976
+ : await callMCPTool('swarm_pheromone_status', {});
977
+ output.writeln(JSON.stringify(data, null, 2));
978
+ return { success: data.active !== false, data };
979
+ },
980
+ };
927
981
  export const swarmCommand = {
928
982
  name: 'swarm',
929
983
  description: 'Swarm coordination commands',
930
- subcommands: [initCommand, startCommand, statusCommand, stopCommand, scaleCommand, coordinateCommand, compressMessageCommand],
984
+ subcommands: [initCommand, startCommand, statusCommand, stopCommand, scaleCommand, coordinateCommand, compressMessageCommand, pheromoneCommand],
931
985
  options: [],
932
986
  examples: [
933
987
  { command: 'claude-flow swarm init --v3-mode', description: 'Initialize V3 swarm' },
934
988
  { command: 'claude-flow swarm start -o "Build API" -s development', description: 'Start development swarm' },
935
- { command: 'claude-flow swarm coordinate --agents 15', description: 'V3 coordination' }
989
+ { command: 'claude-flow swarm coordinate --agents 15', description: 'V3 coordination' },
990
+ { command: 'claude-flow swarm pheromone', description: 'Inspect pheromone-adaptive scheduling state' }
936
991
  ],
937
992
  action: async (ctx) => {
938
993
  output.writeln();
@@ -947,7 +1002,8 @@ export const swarmCommand = {
947
1002
  `${output.highlight('status')} - Show swarm status`,
948
1003
  `${output.highlight('stop')} - Stop swarm execution`,
949
1004
  `${output.highlight('scale')} - Scale swarm agent count`,
950
- `${output.highlight('coordinate')} - V3 15-agent coordination`
1005
+ `${output.highlight('coordinate')} - V3 15-agent coordination`,
1006
+ `${output.highlight('pheromone')} - Inspect/update adaptive pheromone state`
951
1007
  ]);
952
1008
  return { success: true };
953
1009
  }
@@ -147,6 +147,7 @@ function normalizeTopology(topology) {
147
147
  case 'hierarchical-mesh':
148
148
  return topology;
149
149
  case 'adaptive':
150
+ case 'pheromone-adaptive':
150
151
  return 'hybrid';
151
152
  default:
152
153
  return 'hierarchical';
package/dist/src/index.js CHANGED
@@ -196,8 +196,9 @@ export class CLI {
196
196
  try {
197
197
  const { ensureDaemonRunning } = await import('./services/daemon-autostart.js');
198
198
  const d = ensureDaemonRunning(process.cwd());
199
- if (d.started && this.output.isVerbose())
200
- this.output.printDebug('Started background daemon (auto)');
199
+ if (d.started) {
200
+ this.output.printInfo(`Started Ruflo background daemon for ${process.cwd()} (stop: ruflo daemon stop)`);
201
+ }
201
202
  }
202
203
  catch { /* silent */ }
203
204
  }
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { getProjectCwd } from './types.js';
10
10
  import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
11
11
  import { executeAgentTask } from './agent-execute-core.js';
12
+ import { pheromoneAgentEligibility } from './swarm-tools.js';
12
13
  // Storage paths
13
14
  const STORAGE_DIR = '.claude-flow';
14
15
  const AGENT_DIR = 'agents';
@@ -383,6 +384,15 @@ export const agentTools = [
383
384
  const vP = validateText(input.prompt, 'prompt');
384
385
  if (!vP.valid)
385
386
  return { success: false, error: `Input validation failed: ${vP.error}` };
387
+ const pheromone = pheromoneAgentEligibility(input.agentId);
388
+ if (!pheromone.eligible) {
389
+ return {
390
+ success: false,
391
+ agentId: input.agentId,
392
+ suspended: true,
393
+ error: pheromone.reason,
394
+ };
395
+ }
386
396
  // Delegate to the shared core (also used by the workflow runtime).
387
397
  return executeAgentTask({
388
398
  agentId: input.agentId,
@@ -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]);
@@ -946,6 +946,9 @@ export const hooksRoute = {
946
946
  score: 1 - r.score, // Native uses distance (lower is better), convert to similarity
947
947
  metadata: {
948
948
  agents: pattern?.agents || (patternName.startsWith('learned-') ? [patternName.slice(8)] : ['coder']),
949
+ source: pattern?.source ?? (patternName.startsWith('learned-') ? 'learned' : 'static'),
950
+ support: pattern?.support,
951
+ reliability: pattern?.reliability,
949
952
  },
950
953
  };
951
954
  });
@@ -966,8 +969,18 @@ export const hooksRoute = {
966
969
  let agents;
967
970
  let confidence;
968
971
  let matchedPattern = '';
969
- if (semanticResult.length > 0 && semanticResult[0].score > 0.4) {
970
- const topMatch = semanticResult[0];
972
+ const eligibleSemantic = semanticResult.find((match) => {
973
+ if (match.score <= 0.4)
974
+ return false;
975
+ const learned = match.intent.startsWith('learned-') || match.metadata.source === 'learned';
976
+ if (!learned)
977
+ return true;
978
+ return match.score >= 0.65
979
+ && Number(match.metadata.support ?? 0) >= 2
980
+ && Number(match.metadata.reliability ?? 0) >= 0.75;
981
+ });
982
+ if (eligibleSemantic) {
983
+ const topMatch = eligibleSemantic;
971
984
  agents = topMatch.metadata.agents || ['coder', 'researcher'];
972
985
  confidence = topMatch.score;
973
986
  matchedPattern = topMatch.intent;
@@ -1249,6 +1262,10 @@ export const hooksPostTask = {
1249
1262
  agent: { type: 'string', description: 'Agent that completed the task' },
1250
1263
  quality: { type: 'number', description: 'Quality score (0-1)' },
1251
1264
  task: { type: 'string', description: 'Task description text (used for learning keyword extraction)' },
1265
+ duration: { type: 'number', description: 'Observed task duration in milliseconds (used by pheromone-adaptive topology)' },
1266
+ latencyBudgetMs: { type: 'number', description: 'Latency budget used to normalize duration (default 60000ms)' },
1267
+ consensusAlignment: { type: 'number', description: 'Agreement with accepted swarm consensus in [0,1] (default quality)' },
1268
+ agentRole: { type: 'string', description: 'Role for role-local pheromone normalization (default agent)' },
1252
1269
  storeDecisions: { type: 'boolean', description: 'Also store routing decision in memory DB' },
1253
1270
  // ADR-147 P2: nested-subagent spawn-tree capture
1254
1271
  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.' },
@@ -1405,6 +1422,22 @@ export const hooksPostTask = {
1405
1422
  }
1406
1423
  catch { /* non-critical */ }
1407
1424
  }
1425
+ let pheromone;
1426
+ if (agent) {
1427
+ try {
1428
+ const { recordSwarmPheromoneSignal } = await import('./swarm-tools.js');
1429
+ const observedDuration = Math.max(0, Number(params.duration ?? (Date.now() - startTime)));
1430
+ const latencyBudgetMs = Math.max(1, Number(params.latencyBudgetMs ?? 60_000));
1431
+ pheromone = recordSwarmPheromoneSignal({
1432
+ agentId: agent,
1433
+ role: String(params.agentRole ?? agent),
1434
+ taskSuccess: success ? 1 : 0,
1435
+ normalizedLatency: Math.max(0, Math.min(1, observedDuration / latencyBudgetMs)),
1436
+ consensusAlignment: Math.max(0, Math.min(1, Number(params.consensusAlignment ?? quality))),
1437
+ });
1438
+ }
1439
+ catch { /* no active APSC swarm or optional path unavailable */ }
1440
+ }
1408
1441
  const duration = Date.now() - startTime;
1409
1442
  // Persist to auto-memory-store for statusline visibility
1410
1443
  try {
@@ -1444,6 +1477,7 @@ export const hooksPostTask = {
1444
1477
  outcomePersisted,
1445
1478
  },
1446
1479
  quality,
1480
+ pheromone,
1447
1481
  feedback: feedbackResult ? {
1448
1482
  recorded: feedbackResult.success,
1449
1483
  controller: feedbackResult.controller,
@@ -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