@claude-flow/cli 3.43.0 → 3.44.0

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.
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.43.0",
3
+ "version": "3.44.0",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "85fe05c757421c52137c0bc8545a0896bab6b4714538c2a11d1d0835bfcc8c1c",
6
6
  "hook-handler.cjs": "209d9fafe10e17d1be0866727f6f9cf9ac66f9a0793f1c793a4f58319e8e4583",
7
7
  "intelligence.cjs": "30e42ed7ec4ca5a94ac54fdb1330d2d47ac5f3fdeeef207753574723a9e77b5c",
8
- "statusline.cjs": "4a48353b4f1566fa4379b00fd0321b6676a22b6cc91fbbd8380ac5183d619468"
8
+ "statusline.cjs": "4a48353b4f1566fa4379b00fd0321b6676a22b6cc91fbbd8380ac5183d619468",
9
+ "router.js": "b6998397e7883191b62229ccdc66fb87f1ccca1d5b2039ecb8c1e686d1ad81f8"
9
10
  }
10
11
  },
11
- "signature": "OlZvg4x9E5xfjdOatTAJz53uTxtK9dOj56GdkqnvqSWjFOKYCWWmbdijaMeVugo9e/u1y06VtWDGXrKSL82xDA==",
12
+ "signature": "vl9y84CRx7XaIXMi3iD1q3UVqeUvGsvpwjXkhMuA42myuVhCbgtnSNM9W8M3Pv9Odqy5qucwwDsz7MB7mUqDBg==",
12
13
  "algorithm": "ed25519"
13
14
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 6,
4
- "generatedAt": "2026-09-23T15:00:55.000Z",
5
- "gitSha": "025842bb",
4
+ "generatedAt": "2026-09-23T18:40:54.000Z",
5
+ "gitSha": "0a96fb88",
6
6
  "catalog": {
7
7
  "agents": 167,
8
8
  "tools": 418,
@@ -499,7 +499,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
499
499
  const sourceHelpersForUpgrade = findSourceHelpersDir();
500
500
  if (sourceHelpersForUpgrade) {
501
501
  // Keep in sync with helper-refresh.ts:CRITICAL_HELPERS.
502
- const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs'];
502
+ const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.js'];
503
503
  for (const helperName of criticalHelpers) {
504
504
  const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
505
505
  const sourcePath = path.join(sourceHelpersForUpgrade, helperName);
@@ -524,6 +524,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
524
524
  'hook-handler.cjs': generateHookHandler(),
525
525
  'intelligence.cjs': generateIntelligenceStub(),
526
526
  'auto-memory-hook.mjs': generateAutoMemoryHook(),
527
+ 'router.js': generateAgentRouter(), // ADR-389
527
528
  };
528
529
  for (const [helperName, content] of Object.entries(generatedCritical)) {
529
530
  const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
@@ -71,6 +71,10 @@ export const CRITICAL_HELPERS = [
71
71
  // statusline.cjs is here so the funnel disclosure row (ADR-301) reaches
72
72
  // existing installs on the next `ruflo` command, not only fresh `ruflo init`.
73
73
  'statusline.cjs',
74
+ // router.js is loaded by hook-handler.cjs to label each prompt with an agent.
75
+ // Without it here, installs kept the pre-#2257 substring router forever
76
+ // ("latest" -> tester). ADR-389 / #3401.
77
+ 'router.js',
74
78
  ];
75
79
  function errorCode(error) {
76
80
  return typeof error === 'object' && error !== null && 'code' in error
@@ -309,6 +313,7 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
309
313
  'hook-handler.cjs': gen.generateHookHandler(),
310
314
  'intelligence.cjs': gen.generateIntelligenceStub(),
311
315
  'auto-memory-hook.mjs': gen.generateAutoMemoryHook(),
316
+ 'router.js': gen.generateAgentRouter(), // ADR-389
312
317
  // Fallback needs the same generator inputs `ruflo init` uses. We match the
313
318
  // hardcoded default (maxAgents 15) because the fallback fires when the
314
319
  // installed package is unresolvable — no way to read the user's project
@@ -3,6 +3,7 @@
3
3
  * Provides intelligent hooks functionality via MCP protocol
4
4
  */
5
5
  import { type MCPTool } from './types.js';
6
+ import { type RouterEmbedderKind } from '../ruvector/router-embedder.js';
6
7
  /**
7
8
  * Strip extended-thinking blocks from text before it enters a learning
8
9
  * trajectory (hermes-agent think_scrubber pattern). Claude models with extended
@@ -13,6 +14,8 @@ import { type MCPTool } from './types.js';
13
14
  * the tag names untouched.
14
15
  */
15
16
  export declare function scrubReasoningBlocks(text: string): string;
17
+ /** Test hook: drop the cached semantic index so the next route rebuilds it. */
18
+ export declare function resetSemanticRouterForTests(): void;
16
19
  /** Exported for tests. */
17
20
  export declare function suggestAgentsForTask(task: string): {
18
21
  agents: string[];
@@ -23,6 +26,34 @@ export declare const hooksPostEdit: MCPTool;
23
26
  export declare const hooksPreCommand: MCPTool;
24
27
  export declare const hooksPostCommand: MCPTool;
25
28
  export declare const hooksRoute: MCPTool;
29
+ /** Result of {@link routeTaskForBench}. */
30
+ export interface BenchRouteResult {
31
+ primaryAgent: string;
32
+ confidence: number;
33
+ /** Matched pattern name, or 'keyword-fallback'. */
34
+ pattern: string;
35
+ /** Embedder actually used ('hash' if MiniLM was requested but unavailable). */
36
+ embedder: RouterEmbedderKind;
37
+ embedderReason?: string;
38
+ /** routing.method of the underlying route ('semantic-native' | 'semantic-pure-js' | 'keyword'). */
39
+ method: string;
40
+ }
41
+ /**
42
+ * INTERNAL / BENCH-ONLY (ADR-391). Not a public API; may change without notice.
43
+ *
44
+ * Routes `task` through the same local path `hooks_route` uses (semantic index +
45
+ * keyword fallback) with an explicit embedder, without the MCP layer. It skips
46
+ * the two steps that are neither ADR-391 candidate A nor B: the AgentDB
47
+ * pre-route (`bridgeRouteTask`, which answers first when its confidence > 0.5)
48
+ * and the opt-in typesafe wrapper (CLAUDE_FLOW_ROUTER_TYPESAFE). With those
49
+ * inactive, `primaryAgent` equals `hooks_route`'s `primaryAgent.type`.
50
+ *
51
+ * Switching `embedder` between calls rebuilds the index; benchmark in blocks.
52
+ */
53
+ export declare function routeTaskForBench(task: string, opts: {
54
+ embedder: RouterEmbedderKind;
55
+ context?: string;
56
+ }): Promise<BenchRouteResult>;
26
57
  export declare const hooksMetrics: MCPTool;
27
58
  export declare const hooksList: MCPTool;
28
59
  export declare const hooksPreTask: MCPTool;
@@ -10,6 +10,7 @@ import { validateIdentifier, validateText, validatePath } from './validate-input
10
10
  import { checkCommandLoop, recordCommandOutcome } from './tool-loop-guardrail.js';
11
11
  import { buildLearnedRoutingPatterns, } from '../services/learned-routing.js';
12
12
  import { applyTypesafeRouting, getTypesafeRouter } from '../ruvector/typesafe-router.js';
13
+ import { DEFAULT_ROUTER_EMBEDDER, embedForRouter, resolveRouterEmbedder, } from '../ruvector/router-embedder.js';
13
14
  // Real vector search functions - lazy loaded to avoid circular imports
14
15
  let searchEntriesFn = null;
15
16
  /**
@@ -108,46 +109,16 @@ async function getMoERouter() {
108
109
  // Tries native VectorDb first (16k+ routes/s HNSW), falls back to pure JS (47k routes/s cosine)
109
110
  let semanticRouter = null;
110
111
  let nativeVectorDb = null;
111
- let semanticRouterInitialized = false;
112
112
  let routerBackend = 'none';
113
113
  // Pre-computed embeddings for common task patterns (cached)
114
114
  const TASK_PATTERN_EMBEDDINGS = new Map();
115
- function generateSimpleEmbedding(text, dimension = 384) {
116
- // Simple deterministic embedding based on character codes
117
- // This is for routing purposes where we need consistent, fast embeddings
118
- const embedding = new Float32Array(dimension);
119
- const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, '');
120
- const words = normalized.split(/\s+/).filter(w => w.length > 0);
121
- // Combine word-level and character-level features
122
- for (let i = 0; i < dimension; i++) {
123
- let value = 0;
124
- // Word-level features
125
- for (let w = 0; w < words.length; w++) {
126
- const word = words[w];
127
- for (let c = 0; c < word.length; c++) {
128
- const charCode = word.charCodeAt(c);
129
- value += Math.sin((charCode * (i + 1) + w * 17 + c * 23) * 0.0137);
130
- }
131
- }
132
- // Character-level features
133
- for (let c = 0; c < text.length; c++) {
134
- value += Math.cos((text.charCodeAt(c) * (i + 1) + c * 7) * 0.0073);
135
- }
136
- embedding[i] = value / Math.max(1, text.length);
137
- }
138
- // Normalize
139
- let norm = 0;
140
- for (let i = 0; i < dimension; i++) {
141
- norm += embedding[i] * embedding[i];
142
- }
143
- norm = Math.sqrt(norm);
144
- if (norm > 0) {
145
- for (let i = 0; i < dimension; i++) {
146
- embedding[i] /= norm;
147
- }
148
- }
149
- return embedding;
150
- }
115
+ // ADR-390: the router's embedder (MiniLM or the historical hash). Pattern and
116
+ // query vectors always come from the SAME embedder; `routerEmbedder` records the
117
+ // one that actually built the current index (after any degradation to hash).
118
+ let routerEmbedder = DEFAULT_ROUTER_EMBEDDER;
119
+ let routerEmbedderReason;
120
+ let routerRequestedEmbedder = null;
121
+ let routerInitPromise = null;
151
122
  // ── Runtime routing outcome persistence ──────────────────────────────
152
123
  // Closes the learning loop: post-task records outcomes → route loads them.
153
124
  const ROUTING_OUTCOMES_PATH = join(resolve('.'), '.claude-flow/routing-outcomes.json');
@@ -191,7 +162,8 @@ function saveRoutingOutcomes(outcomes) {
191
162
  // The prior singleton cache made the learned store inert until restart.
192
163
  semanticRouter = null;
193
164
  nativeVectorDb = null;
194
- semanticRouterInitialized = false;
165
+ routerInitPromise = null;
166
+ routerRequestedEmbedder = null;
195
167
  routerBackend = 'none';
196
168
  TASK_PATTERN_EMBEDDINGS.clear();
197
169
  }
@@ -277,12 +249,58 @@ function withTypesafeRouting(legacy) {
277
249
  /**
278
250
  * Get the semantic router with environment detection.
279
251
  * Tries native VectorDb first (HNSW, 16k routes/s), falls back to pure JS (47k routes/s cosine).
252
+ *
253
+ * ADR-390: `requested` (else CLAUDE_FLOW_ROUTER_EMBEDDER, else the default)
254
+ * picks the embedder. The index is rebuilt when the requested embedder changes.
255
+ * Concurrent callers share one in-flight build.
280
256
  */
281
- async function getSemanticRouter() {
282
- if (semanticRouterInitialized) {
283
- return { router: semanticRouter, backend: routerBackend, native: nativeVectorDb };
257
+ async function getSemanticRouter(requested) {
258
+ const selection = resolveRouterEmbedder(requested);
259
+ if (routerInitPromise && routerRequestedEmbedder === selection.kind) {
260
+ return routerInitPromise;
284
261
  }
285
- semanticRouterInitialized = true;
262
+ semanticRouter = null;
263
+ nativeVectorDb = null;
264
+ routerBackend = 'none';
265
+ TASK_PATTERN_EMBEDDINGS.clear();
266
+ routerRequestedEmbedder = selection.kind;
267
+ routerInitPromise = buildSemanticRouter(selection.kind, selection.reason);
268
+ return routerInitPromise;
269
+ }
270
+ /** Test hook: drop the cached semantic index so the next route rebuilds it. */
271
+ export function resetSemanticRouterForTests() {
272
+ semanticRouter = null;
273
+ nativeVectorDb = null;
274
+ routerBackend = 'none';
275
+ routerInitPromise = null;
276
+ routerRequestedEmbedder = null;
277
+ TASK_PATTERN_EMBEDDINGS.clear();
278
+ }
279
+ /** Embed every pattern keyword with ONE embedder (ADR-390: never mix spaces). */
280
+ async function embedPatternKeywords(patterns, kind) {
281
+ const entries = Object.entries(patterns);
282
+ const flat = entries.flatMap(([, p]) => p.keywords);
283
+ const res = await embedForRouter(flat, kind);
284
+ const vectors = new Map();
285
+ let i = 0;
286
+ for (const [name, p] of entries) {
287
+ vectors.set(name, res.vectors.slice(i, i + p.keywords.length));
288
+ i += p.keywords.length;
289
+ }
290
+ return { vectors, embedder: res.embedder, reason: res.reason };
291
+ }
292
+ async function buildSemanticRouter(kind, selectionReason) {
293
+ const patterns = getMergedTaskPatterns();
294
+ const embedded = await embedPatternKeywords(patterns, kind);
295
+ routerEmbedder = embedded.embedder;
296
+ routerEmbedderReason = embedded.reason ?? selectionReason;
297
+ const handle = () => ({
298
+ router: semanticRouter,
299
+ backend: routerBackend,
300
+ native: nativeVectorDb,
301
+ embedder: routerEmbedder,
302
+ ...(routerEmbedderReason ? { embedderReason: routerEmbedderReason } : {}),
303
+ });
286
304
  // STEP 1: Try native VectorDb from @ruvector/router (HNSW-backed)
287
305
  // Note: Native VectorDb uses a persistent database file which can have lock issues
288
306
  // in concurrent environments. We try it first but fall back gracefully to pure JS.
@@ -304,17 +322,17 @@ async function getSemanticRouter() {
304
322
  hnswEfSearch: 100,
305
323
  });
306
324
  // Initialize with static + runtime-learned task patterns
307
- for (const [patternName, { keywords }] of Object.entries(getMergedTaskPatterns())) {
308
- for (const keyword of keywords) {
309
- const embedding = generateSimpleEmbedding(keyword);
310
- db.insert(`${patternName}:${keyword}`, embedding);
311
- TASK_PATTERN_EMBEDDINGS.set(`${patternName}:${keyword}`, embedding);
312
- }
325
+ for (const [patternName, { keywords }] of Object.entries(patterns)) {
326
+ const embeddings = embedded.vectors.get(patternName) ?? [];
327
+ keywords.forEach((keyword, i) => {
328
+ db.insert(`${patternName}:${keyword}`, embeddings[i]);
329
+ TASK_PATTERN_EMBEDDINGS.set(`${patternName}:${keyword}`, embeddings[i]);
330
+ });
313
331
  }
314
332
  nativeVectorDb = db;
315
333
  routerBackend = 'native';
316
- console.log('[hooks] Semantic router initialized: native VectorDb (HNSW, 16k+ routes/s)');
317
- return { router: null, backend: routerBackend, native: nativeVectorDb };
334
+ console.log(`[hooks] Semantic router initialized: native VectorDb (HNSW, 16k+ routes/s), embedder=${routerEmbedder}`);
335
+ return handle();
318
336
  }
319
337
  }
320
338
  catch (err) {
@@ -326,8 +344,8 @@ async function getSemanticRouter() {
326
344
  try {
327
345
  const { SemanticRouter } = await import('../ruvector/semantic-router.js');
328
346
  semanticRouter = new SemanticRouter({ dimension: 384 });
329
- for (const [patternName, { keywords, agents, source, support, reliability }] of Object.entries(getMergedTaskPatterns())) {
330
- const embeddings = keywords.map(kw => generateSimpleEmbedding(kw));
347
+ for (const [patternName, { keywords, agents, source, support, reliability }] of Object.entries(patterns)) {
348
+ const embeddings = embedded.vectors.get(patternName) ?? [];
331
349
  semanticRouter.addIntentWithEmbeddings(patternName, embeddings, {
332
350
  agents,
333
351
  keywords,
@@ -341,14 +359,14 @@ async function getSemanticRouter() {
341
359
  });
342
360
  }
343
361
  routerBackend = 'pure-js';
344
- console.log('[hooks] Semantic router initialized: pure JS (cosine, 47k routes/s)');
362
+ console.log(`[hooks] Semantic router initialized: pure JS (cosine, 47k routes/s), embedder=${routerEmbedder}`);
345
363
  }
346
364
  catch {
347
365
  semanticRouter = null;
348
366
  routerBackend = 'none';
349
367
  console.log('[hooks] Semantic router initialized: none (no backend available)');
350
368
  }
351
- return { router: semanticRouter, backend: routerBackend, native: nativeVectorDb };
369
+ return handle();
352
370
  }
353
371
  /**
354
372
  * Get router backend info for status display.
@@ -1021,134 +1039,185 @@ export const hooksRoute = {
1021
1039
  // AgentDB router not available — fall through to local routing
1022
1040
  }
1023
1041
  }
1024
- // Get router (tries native VectorDb first, falls back to pure JS)
1025
- const { router, backend, native } = useSemanticRouter
1026
- ? await getSemanticRouter()
1027
- : { router: null, backend: 'none', native: null };
1028
- let semanticResult = [];
1029
- let routingMethod = 'keyword';
1030
- let routingLatencyMs = 0;
1031
- let backendInfo = '';
1032
- const queryText = context ? `${task} ${context}` : task;
1033
- const queryEmbedding = generateSimpleEmbedding(queryText);
1034
- // Try native VectorDb (HNSW-backed)
1035
- if (native && backend === 'native') {
1036
- const routeStart = performance.now();
1037
- try {
1038
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1039
- const results = native.search(queryEmbedding, 5);
1040
- routingLatencyMs = performance.now() - routeStart;
1041
- routingMethod = 'semantic-native';
1042
- backendInfo = 'native VectorDb (HNSW)';
1043
- // Convert results to semantic format
1044
- const mergedPatterns = getMergedTaskPatterns();
1045
- semanticResult = results.map((r) => {
1046
- const [patternName] = r.id.split(':');
1047
- const pattern = mergedPatterns[patternName];
1048
- return {
1049
- intent: patternName,
1050
- score: 1 - r.score, // Native uses distance (lower is better), convert to similarity
1051
- metadata: {
1052
- agents: pattern?.agents || (patternName.startsWith('learned-') ? [patternName.slice(8)] : ['coder']),
1053
- source: pattern?.source ?? (patternName.startsWith('learned-') ? 'learned' : 'static'),
1054
- support: pattern?.support,
1055
- reliability: pattern?.reliability,
1056
- },
1057
- };
1058
- });
1059
- }
1060
- catch {
1061
- // Native failed, try pure JS fallback
1062
- }
1063
- }
1064
- // Try pure JS SemanticRouter fallback
1065
- if (router && backend === 'pure-js' && semanticResult.length === 0) {
1066
- const routeStart = performance.now();
1067
- semanticResult = router.routeWithEmbedding(queryEmbedding, 3);
1042
+ return routeTaskLocal(task, context, useSemanticRouter);
1043
+ }),
1044
+ };
1045
+ /**
1046
+ * hooks_route's local routing (semantic index + keyword fallback), run after the
1047
+ * AgentDB pre-route. Shared with {@link routeTaskForBench} so the benchmark
1048
+ * measures exactly the path hooks_route takes. (Body kept at its original
1049
+ * indentation to keep this refactor's diff small.)
1050
+ */
1051
+ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride) {
1052
+ // Get router (tries native VectorDb first, falls back to pure JS)
1053
+ let handle = useSemanticRouter
1054
+ ? await getSemanticRouter(embedderOverride)
1055
+ : {
1056
+ router: null, backend: 'none', native: null,
1057
+ embedder: resolveRouterEmbedder(embedderOverride).kind,
1058
+ embedderReason: 'semantic router disabled (useSemanticRouter=false)',
1059
+ };
1060
+ let semanticResult = [];
1061
+ let routingMethod = 'keyword';
1062
+ let routingLatencyMs = 0;
1063
+ let backendInfo = '';
1064
+ const queryText = context ? `${task} ${context}` : task;
1065
+ // ADR-390: the query is embedded with the embedder that built the index.
1066
+ let queryEmbedding = null;
1067
+ if (handle.router || handle.native) {
1068
+ const q = await embedForRouter([queryText], handle.embedder);
1069
+ if (q.embedder !== handle.embedder) {
1070
+ // MiniLM failed on the query after building a MiniLM index: rebuild the
1071
+ // index with the hash so patterns and query share one space again.
1072
+ routerInitPromise = buildSemanticRouter('hash', q.reason);
1073
+ handle = await routerInitPromise;
1074
+ }
1075
+ queryEmbedding = q.vectors[0];
1076
+ }
1077
+ const { router, backend, native } = handle;
1078
+ // Try native VectorDb (HNSW-backed)
1079
+ if (native && backend === 'native' && queryEmbedding) {
1080
+ const routeStart = performance.now();
1081
+ try {
1082
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1083
+ const results = native.search(queryEmbedding, 5);
1068
1084
  routingLatencyMs = performance.now() - routeStart;
1069
- routingMethod = 'semantic-pure-js';
1070
- backendInfo = 'pure JS (cosine similarity)';
1071
- }
1072
- // Get agents from semantic routing or fall back to keyword
1073
- let agents;
1074
- let confidence;
1075
- let matchedPattern = '';
1076
- // Both static and learned patterns are gated on the same similarity
1077
- // score. Learned patterns additionally require support/reliability as a
1078
- // quality guard, but do NOT need a higher score bar — a learned pattern
1079
- // that outscores every static candidate must not lose to one anyway
1080
- // (#2864: a 25pp higher threshold made a top-scoring learned-researcher
1081
- // match at 0.57 lose to a static match at 0.52, discarding the learned
1082
- // store's output on the majority of routes).
1083
- const eligibleSemantic = semanticResult.find((match) => {
1084
- if (match.score <= 0.4)
1085
- return false;
1086
- const learned = match.intent.startsWith('learned-') || match.metadata.source === 'learned';
1087
- if (!learned)
1088
- return true;
1089
- return Number(match.metadata.support ?? 0) >= 2
1090
- && Number(match.metadata.reliability ?? 0) >= 0.75;
1091
- });
1092
- if (eligibleSemantic) {
1093
- const topMatch = eligibleSemantic;
1094
- agents = topMatch.metadata.agents || ['coder', 'researcher'];
1095
- confidence = topMatch.score;
1096
- matchedPattern = topMatch.intent;
1085
+ routingMethod = 'semantic-native';
1086
+ backendInfo = 'native VectorDb (HNSW)';
1087
+ // Convert results to semantic format
1088
+ const mergedPatterns = getMergedTaskPatterns();
1089
+ semanticResult = results.map((r) => {
1090
+ const [patternName] = r.id.split(':');
1091
+ const pattern = mergedPatterns[patternName];
1092
+ return {
1093
+ intent: patternName,
1094
+ score: 1 - r.score, // Native uses distance (lower is better), convert to similarity
1095
+ metadata: {
1096
+ agents: pattern?.agents || (patternName.startsWith('learned-') ? [patternName.slice(8)] : ['coder']),
1097
+ source: pattern?.source ?? (patternName.startsWith('learned-') ? 'learned' : 'static'),
1098
+ support: pattern?.support,
1099
+ reliability: pattern?.reliability,
1100
+ },
1101
+ };
1102
+ });
1097
1103
  }
1098
- else {
1099
- // Fall back to keyword matching
1100
- const suggestion = suggestAgentsForTask(task);
1101
- agents = suggestion.agents;
1102
- confidence = suggestion.confidence;
1103
- matchedPattern = 'keyword-fallback';
1104
- routingMethod = 'keyword';
1105
- backendInfo = 'keyword matching';
1104
+ catch {
1105
+ // Native failed, try pure JS fallback
1106
1106
  }
1107
- // Determine complexity
1108
- const taskLower = task.toLowerCase();
1109
- const complexity = taskLower.includes('complex') || taskLower.includes('architecture') || task.length > 200
1110
- ? 'high'
1111
- : taskLower.includes('simple') || taskLower.includes('fix') || task.length < 50
1112
- ? 'low'
1113
- : 'medium';
1114
- return {
1115
- task,
1116
- routing: {
1117
- method: routingMethod,
1118
- backend: backendInfo,
1119
- latencyMs: routingLatencyMs,
1120
- throughput: routingLatencyMs > 0 ? `${Math.round(1000 / routingLatencyMs)} routes/s` : 'N/A',
1121
- },
1122
- matchedPattern,
1123
- semanticMatches: semanticResult.slice(0, 3).map(r => ({
1124
- pattern: r.intent,
1125
- score: Math.round(r.score * 100) / 100,
1126
- })),
1127
- primaryAgent: {
1128
- type: agents[0],
1129
- confidence: Math.round(confidence * 100) / 100,
1130
- reason: routingMethod.startsWith('semantic')
1131
- ? `Semantic similarity to "${matchedPattern}" pattern (${Math.round(confidence * 100)}%)`
1132
- : `Task contains keywords matching ${agents[0]} specialization`,
1133
- },
1134
- alternativeAgents: agents.slice(1).map((agent, i) => ({
1135
- type: agent,
1136
- confidence: Math.round((confidence - (0.1 * (i + 1))) * 100) / 100,
1137
- reason: `Alternative agent for ${agent} capabilities`,
1138
- })),
1139
- estimatedMetrics: {
1140
- successProbability: Math.round(confidence * 100) / 100,
1141
- estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
1142
- complexity,
1143
- },
1144
- swarmRecommendation: agents.length > 2 ? {
1145
- topology: 'hierarchical',
1146
- agents,
1147
- coordination: 'queen-led',
1148
- } : null,
1149
- };
1150
- }),
1151
- };
1107
+ }
1108
+ // Try pure JS SemanticRouter fallback
1109
+ if (router && backend === 'pure-js' && queryEmbedding && semanticResult.length === 0) {
1110
+ const routeStart = performance.now();
1111
+ semanticResult = router.routeWithEmbedding(queryEmbedding, 3);
1112
+ routingLatencyMs = performance.now() - routeStart;
1113
+ routingMethod = 'semantic-pure-js';
1114
+ backendInfo = 'pure JS (cosine similarity)';
1115
+ }
1116
+ // Get agents from semantic routing or fall back to keyword
1117
+ let agents;
1118
+ let confidence;
1119
+ let matchedPattern = '';
1120
+ // Both static and learned patterns are gated on the same similarity
1121
+ // score. Learned patterns additionally require support/reliability as a
1122
+ // quality guard, but do NOT need a higher score bar — a learned pattern
1123
+ // that outscores every static candidate must not lose to one anyway
1124
+ // (#2864: a 25pp higher threshold made a top-scoring learned-researcher
1125
+ // match at 0.57 lose to a static match at 0.52, discarding the learned
1126
+ // store's output on the majority of routes).
1127
+ const eligibleSemantic = semanticResult.find((match) => {
1128
+ if (match.score <= 0.4)
1129
+ return false;
1130
+ const learned = match.intent.startsWith('learned-') || match.metadata.source === 'learned';
1131
+ if (!learned)
1132
+ return true;
1133
+ return Number(match.metadata.support ?? 0) >= 2
1134
+ && Number(match.metadata.reliability ?? 0) >= 0.75;
1135
+ });
1136
+ if (eligibleSemantic) {
1137
+ const topMatch = eligibleSemantic;
1138
+ agents = topMatch.metadata.agents || ['coder', 'researcher'];
1139
+ confidence = topMatch.score;
1140
+ matchedPattern = topMatch.intent;
1141
+ }
1142
+ else {
1143
+ // Fall back to keyword matching
1144
+ const suggestion = suggestAgentsForTask(task);
1145
+ agents = suggestion.agents;
1146
+ confidence = suggestion.confidence;
1147
+ matchedPattern = 'keyword-fallback';
1148
+ routingMethod = 'keyword';
1149
+ backendInfo = 'keyword matching';
1150
+ }
1151
+ // Determine complexity
1152
+ const taskLower = task.toLowerCase();
1153
+ const complexity = taskLower.includes('complex') || taskLower.includes('architecture') || task.length > 200
1154
+ ? 'high'
1155
+ : taskLower.includes('simple') || taskLower.includes('fix') || task.length < 50
1156
+ ? 'low'
1157
+ : 'medium';
1158
+ return {
1159
+ task,
1160
+ routing: {
1161
+ method: routingMethod,
1162
+ backend: backendInfo,
1163
+ latencyMs: routingLatencyMs,
1164
+ throughput: routingLatencyMs > 0 ? `${Math.round(1000 / routingLatencyMs)} routes/s` : 'N/A',
1165
+ },
1166
+ matchedPattern,
1167
+ semanticMatches: semanticResult.slice(0, 3).map(r => ({
1168
+ pattern: r.intent,
1169
+ score: Math.round(r.score * 100) / 100,
1170
+ })),
1171
+ primaryAgent: {
1172
+ type: agents[0],
1173
+ confidence: Math.round(confidence * 100) / 100,
1174
+ reason: routingMethod.startsWith('semantic')
1175
+ ? `Semantic similarity to "${matchedPattern}" pattern (${Math.round(confidence * 100)}%)`
1176
+ : `Task contains keywords matching ${agents[0]} specialization`,
1177
+ },
1178
+ alternativeAgents: agents.slice(1).map((agent, i) => ({
1179
+ type: agent,
1180
+ confidence: Math.round((confidence - (0.1 * (i + 1))) * 100) / 100,
1181
+ reason: `Alternative agent for ${agent} capabilities`,
1182
+ })),
1183
+ estimatedMetrics: {
1184
+ successProbability: Math.round(confidence * 100) / 100,
1185
+ estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
1186
+ complexity,
1187
+ },
1188
+ swarmRecommendation: agents.length > 2 ? {
1189
+ topology: 'hierarchical',
1190
+ agents,
1191
+ coordination: 'queen-led',
1192
+ } : null,
1193
+ // ADR-390: which embedder the semantic index + query used ('hash' when degraded).
1194
+ embedder: handle.embedder,
1195
+ ...(handle.embedderReason ? { embedderReason: handle.embedderReason } : {}),
1196
+ };
1197
+ }
1198
+ /**
1199
+ * INTERNAL / BENCH-ONLY (ADR-391). Not a public API; may change without notice.
1200
+ *
1201
+ * Routes `task` through the same local path `hooks_route` uses (semantic index +
1202
+ * keyword fallback) with an explicit embedder, without the MCP layer. It skips
1203
+ * the two steps that are neither ADR-391 candidate A nor B: the AgentDB
1204
+ * pre-route (`bridgeRouteTask`, which answers first when its confidence > 0.5)
1205
+ * and the opt-in typesafe wrapper (CLAUDE_FLOW_ROUTER_TYPESAFE). With those
1206
+ * inactive, `primaryAgent` equals `hooks_route`'s `primaryAgent.type`.
1207
+ *
1208
+ * Switching `embedder` between calls rebuilds the index; benchmark in blocks.
1209
+ */
1210
+ export async function routeTaskForBench(task, opts) {
1211
+ const r = await routeTaskLocal(task, opts.context, true, opts.embedder);
1212
+ return {
1213
+ primaryAgent: r.primaryAgent.type,
1214
+ confidence: r.primaryAgent.confidence,
1215
+ pattern: r.matchedPattern,
1216
+ embedder: r.embedder,
1217
+ ...(r.embedderReason ? { embedderReason: r.embedderReason } : {}),
1218
+ method: String(r.routing?.method ?? ''),
1219
+ };
1220
+ }
1152
1221
  export const hooksMetrics = {
1153
1222
  name: 'hooks_metrics',
1154
1223
  description: 'View learning metrics dashboard Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.',
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Router embedder (ADR-390).
3
+ *
4
+ * `hooks_route` compares a task with each agent pattern's keywords in a 384-d
5
+ * vector index. Historically both sides came from a character hash
6
+ * (`generateSimpleEmbedding`), which measures spelling, not meaning. This
7
+ * module lets the router use the local sentence model (all-MiniLM-L6-v2, 384-d)
8
+ * instead, with the hash as the fallback.
9
+ *
10
+ * Rules (ADR-390 §Decision):
11
+ * - The MiniLM path uses `generateLocalEmbedding` ONLY. Never the bridge-first
12
+ * `generateEmbedding` — that recursed without bound in #2312.
13
+ * - One embedder per index: if ANY text cannot be embedded by the real model
14
+ * (throw, backend !== 'onnx', or a non-384 vector), EVERY text in the call
15
+ * is embedded with the hash, and the result says so.
16
+ * - The default stays `hash` until ADR-391's benchmark says otherwise.
17
+ *
18
+ * Selection: `CLAUDE_FLOW_ROUTER_EMBEDDER=minilm|hash`. The router index is
19
+ * process-lifetime state, so the env var is read when the index is (re)built,
20
+ * not per CLI invocation.
21
+ */
22
+ export type RouterEmbedderKind = 'minilm' | 'hash';
23
+ /** Default embedder. ADR-391 decides whether this flips to 'minilm'. */
24
+ export declare const DEFAULT_ROUTER_EMBEDDER: RouterEmbedderKind;
25
+ /** Dimension of the router index (VectorDb + SemanticRouter are built at 384). */
26
+ export declare const ROUTER_EMBEDDING_DIM = 384;
27
+ export declare const ROUTER_EMBEDDER_ENV = "CLAUDE_FLOW_ROUTER_EMBEDDER";
28
+ export interface RouterEmbedderSelection {
29
+ kind: RouterEmbedderKind;
30
+ /** Set when the requested value was invalid and the default was used. */
31
+ reason?: string;
32
+ }
33
+ export interface RouterEmbeddingResult {
34
+ vectors: Float32Array[];
35
+ /** The embedder that actually produced `vectors` (after any degradation). */
36
+ embedder: RouterEmbedderKind;
37
+ /** Why the result is `hash` when `minilm` was requested. */
38
+ reason?: string;
39
+ }
40
+ /**
41
+ * Resolve which embedder the router should use.
42
+ * Precedence: explicit override > CLAUDE_FLOW_ROUTER_EMBEDDER > DEFAULT_ROUTER_EMBEDDER.
43
+ */
44
+ export declare function resolveRouterEmbedder(override?: RouterEmbedderKind, env?: NodeJS.ProcessEnv): RouterEmbedderSelection;
45
+ /**
46
+ * Deterministic character-hash embedding (the router's historical embedder).
47
+ * Moved verbatim from hooks-tools.ts; do not change the math — existing routes
48
+ * and thresholds were calibrated against it.
49
+ */
50
+ export declare function generateSimpleEmbedding(text: string, dimension?: number): Float32Array;
51
+ /**
52
+ * Embed texts for the router. All vectors in one result come from ONE embedder.
53
+ * `hash` never touches the model (no load cost for default users).
54
+ */
55
+ export declare function embedForRouter(texts: readonly string[], kind?: RouterEmbedderKind): Promise<RouterEmbeddingResult>;
56
+ /** Test hook: clear the MiniLM vector memo. */
57
+ export declare function clearRouterEmbedderCache(): void;
58
+ //# sourceMappingURL=router-embedder.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Router embedder (ADR-390).
3
+ *
4
+ * `hooks_route` compares a task with each agent pattern's keywords in a 384-d
5
+ * vector index. Historically both sides came from a character hash
6
+ * (`generateSimpleEmbedding`), which measures spelling, not meaning. This
7
+ * module lets the router use the local sentence model (all-MiniLM-L6-v2, 384-d)
8
+ * instead, with the hash as the fallback.
9
+ *
10
+ * Rules (ADR-390 §Decision):
11
+ * - The MiniLM path uses `generateLocalEmbedding` ONLY. Never the bridge-first
12
+ * `generateEmbedding` — that recursed without bound in #2312.
13
+ * - One embedder per index: if ANY text cannot be embedded by the real model
14
+ * (throw, backend !== 'onnx', or a non-384 vector), EVERY text in the call
15
+ * is embedded with the hash, and the result says so.
16
+ * - The default stays `hash` until ADR-391's benchmark says otherwise.
17
+ *
18
+ * Selection: `CLAUDE_FLOW_ROUTER_EMBEDDER=minilm|hash`. The router index is
19
+ * process-lifetime state, so the env var is read when the index is (re)built,
20
+ * not per CLI invocation.
21
+ */
22
+ // memory-initializer is imported lazily (as hooks-tools does elsewhere): it is
23
+ // heavy, and the default `hash` path must not load it at all.
24
+ /** Default embedder. ADR-391 decides whether this flips to 'minilm'. */
25
+ export const DEFAULT_ROUTER_EMBEDDER = 'hash';
26
+ /** Dimension of the router index (VectorDb + SemanticRouter are built at 384). */
27
+ export const ROUTER_EMBEDDING_DIM = 384;
28
+ export const ROUTER_EMBEDDER_ENV = 'CLAUDE_FLOW_ROUTER_EMBEDDER';
29
+ /**
30
+ * Resolve which embedder the router should use.
31
+ * Precedence: explicit override > CLAUDE_FLOW_ROUTER_EMBEDDER > DEFAULT_ROUTER_EMBEDDER.
32
+ */
33
+ export function resolveRouterEmbedder(override, env = process.env) {
34
+ if (override)
35
+ return { kind: override };
36
+ // Env-only by design (registered in scripts/audit-env-var-precedence.mjs):
37
+ // the router index is process-lifetime MCP state, not owned by one CLI call.
38
+ const raw = env.CLAUDE_FLOW_ROUTER_EMBEDDER?.trim().toLowerCase();
39
+ if (!raw)
40
+ return { kind: DEFAULT_ROUTER_EMBEDDER };
41
+ if (raw === 'minilm' || raw === 'hash')
42
+ return { kind: raw };
43
+ return {
44
+ kind: DEFAULT_ROUTER_EMBEDDER,
45
+ reason: `${ROUTER_EMBEDDER_ENV}=${JSON.stringify(raw)} is not 'minilm' or 'hash'; using '${DEFAULT_ROUTER_EMBEDDER}'`,
46
+ };
47
+ }
48
+ /**
49
+ * Deterministic character-hash embedding (the router's historical embedder).
50
+ * Moved verbatim from hooks-tools.ts; do not change the math — existing routes
51
+ * and thresholds were calibrated against it.
52
+ */
53
+ export function generateSimpleEmbedding(text, dimension = ROUTER_EMBEDDING_DIM) {
54
+ const embedding = new Float32Array(dimension);
55
+ const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, '');
56
+ const words = normalized.split(/\s+/).filter(w => w.length > 0);
57
+ for (let i = 0; i < dimension; i++) {
58
+ let value = 0;
59
+ // Word-level features
60
+ for (let w = 0; w < words.length; w++) {
61
+ const word = words[w];
62
+ for (let c = 0; c < word.length; c++) {
63
+ const charCode = word.charCodeAt(c);
64
+ value += Math.sin((charCode * (i + 1) + w * 17 + c * 23) * 0.0137);
65
+ }
66
+ }
67
+ // Character-level features
68
+ for (let c = 0; c < text.length; c++) {
69
+ value += Math.cos((text.charCodeAt(c) * (i + 1) + c * 7) * 0.0073);
70
+ }
71
+ embedding[i] = value / Math.max(1, text.length);
72
+ }
73
+ return l2Normalize(embedding);
74
+ }
75
+ function l2Normalize(v) {
76
+ let norm = 0;
77
+ for (let i = 0; i < v.length; i++)
78
+ norm += v[i] * v[i];
79
+ norm = Math.sqrt(norm);
80
+ if (norm > 0)
81
+ for (let i = 0; i < v.length; i++)
82
+ v[i] /= norm;
83
+ return v;
84
+ }
85
+ // Per-(embedder, text) memo. Keyword vectors don't change when the pattern set
86
+ // changes, so this survives router rebuilds (e.g. after saveRoutingOutcomes).
87
+ const MINILM_CACHE = new Map();
88
+ const MINILM_CACHE_MAX = 2048;
89
+ async function embedOneMiniLM(text) {
90
+ const cached = MINILM_CACHE.get(text);
91
+ if (cached)
92
+ return cached;
93
+ const { generateLocalEmbedding } = await import('../memory/memory-initializer.js');
94
+ const out = await generateLocalEmbedding(text);
95
+ if (out.backend !== 'onnx') {
96
+ throw new RouterEmbedderDegraded(`local embedder backend is '${out.backend}' (model '${out.model}'), not onnx`);
97
+ }
98
+ if (!out.embedding || out.embedding.length !== ROUTER_EMBEDDING_DIM) {
99
+ throw new RouterEmbedderDegraded(`local embedder returned ${out.embedding?.length ?? 0}-d vectors; router index is ${ROUTER_EMBEDDING_DIM}-d`);
100
+ }
101
+ const vec = l2Normalize(Float32Array.from(out.embedding));
102
+ if (MINILM_CACHE.size >= MINILM_CACHE_MAX) {
103
+ const oldest = MINILM_CACHE.keys().next().value;
104
+ if (oldest !== undefined)
105
+ MINILM_CACHE.delete(oldest);
106
+ }
107
+ MINILM_CACHE.set(text, vec);
108
+ return vec;
109
+ }
110
+ class RouterEmbedderDegraded extends Error {
111
+ }
112
+ /**
113
+ * Embed texts for the router. All vectors in one result come from ONE embedder.
114
+ * `hash` never touches the model (no load cost for default users).
115
+ */
116
+ export async function embedForRouter(texts, kind = resolveRouterEmbedder().kind) {
117
+ if (kind === 'hash') {
118
+ return { vectors: texts.map(t => generateSimpleEmbedding(t)), embedder: 'hash' };
119
+ }
120
+ try {
121
+ const vectors = [];
122
+ for (const t of texts)
123
+ vectors.push(await embedOneMiniLM(t));
124
+ return { vectors, embedder: 'minilm' };
125
+ }
126
+ catch (err) {
127
+ const why = err instanceof RouterEmbedderDegraded
128
+ ? err.message
129
+ : `local embedder threw: ${err instanceof Error ? err.message : String(err)}`;
130
+ return {
131
+ vectors: texts.map(t => generateSimpleEmbedding(t)),
132
+ embedder: 'hash',
133
+ reason: `minilm unavailable (${why}); using hash for patterns and query`,
134
+ };
135
+ }
136
+ }
137
+ /** Test hook: clear the MiniLM vector memo. */
138
+ export function clearRouterEmbedderCache() {
139
+ MINILM_CACHE.clear();
140
+ }
141
+ //# sourceMappingURL=router-embedder.js.map
File without changes
@@ -159,14 +159,14 @@ export declare const SpawnAgentSchema: z.ZodObject<{
159
159
  timeout: z.ZodOptional<z.ZodNumber>;
160
160
  }, "strip", z.ZodTypeAny, {
161
161
  type: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
162
- id?: string | undefined;
163
162
  config?: Record<string, unknown> | undefined;
164
163
  timeout?: number | undefined;
164
+ id?: string | undefined;
165
165
  }, {
166
166
  type: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
167
- id?: string | undefined;
168
167
  config?: Record<string, unknown> | undefined;
169
168
  timeout?: number | undefined;
169
+ id?: string | undefined;
170
170
  }>;
171
171
  /**
172
172
  * Task input schema
@@ -181,13 +181,13 @@ export declare const TaskInputSchema: z.ZodObject<{
181
181
  taskId: string;
182
182
  content: string;
183
183
  agentType: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
184
- priority?: "low" | "medium" | "high" | "critical" | undefined;
184
+ priority?: "critical" | "high" | "medium" | "low" | undefined;
185
185
  metadata?: Record<string, unknown> | undefined;
186
186
  }, {
187
187
  taskId: string;
188
188
  content: string;
189
189
  agentType: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
190
- priority?: "low" | "medium" | "high" | "critical" | undefined;
190
+ priority?: "critical" | "high" | "medium" | "low" | undefined;
191
191
  metadata?: Record<string, unknown> | undefined;
192
192
  }>;
193
193
  /**
@@ -234,16 +234,16 @@ export declare const ExecutorConfigSchema: z.ZodObject<{
234
234
  cwd: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
235
235
  allowSudo: z.ZodDefault<z.ZodBoolean>;
236
236
  }, "strip", z.ZodTypeAny, {
237
- timeout: number;
238
237
  allowedCommands: string[];
238
+ timeout: number;
239
239
  maxBuffer: number;
240
240
  allowSudo: boolean;
241
241
  blockedPatterns?: string[] | undefined;
242
242
  cwd?: string | undefined;
243
243
  }, {
244
244
  allowedCommands: string[];
245
- timeout?: number | undefined;
246
245
  blockedPatterns?: string[] | undefined;
246
+ timeout?: number | undefined;
247
247
  maxBuffer?: number | undefined;
248
248
  cwd?: string | undefined;
249
249
  allowSudo?: boolean | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.43.0",
3
+ "version": "3.44.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",