@claude-flow/cli 3.43.0 → 3.45.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.45.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": "IO42tJDeyKIl8l7ewYgoPdlZqQdsESvjIJr3KoAHDYHTi7gMCElwEyah4jx/RC6+Lq2zXv8XSOd/cgHI2U5zAg==",
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-24T22:48:10.625Z",
5
+ "gitSha": "aab1d625",
6
6
  "catalog": {
7
7
  "agents": 167,
8
8
  "tools": 418,
@@ -12,6 +12,15 @@ interface HealthCheck {
12
12
  fix?: string;
13
13
  }
14
14
  export declare function checkMemoryPersistenceDriver(): Promise<HealthCheck>;
15
+ /**
16
+ * #3392: pure verdict for "does the @claude-flow/memory the CLI loads satisfy
17
+ * the range the CLI declares?". `npx @claude-flow/cli@latest` reuses one npx
18
+ * cache directory across CLI versions, and npm keeps an already-installed
19
+ * dependency that still satisfies a caret range, so a stale memory could
20
+ * survive a CLI upgrade with no error. Exported for unit testing.
21
+ */
22
+ export declare function evaluateMemoryPackageVersion(declared: string | null, installed: string | null): HealthCheck;
23
+ export declare function checkMemoryPackageVersion(): Promise<HealthCheck>;
15
24
  export declare const doctorCommand: Command;
16
25
  export default doctorCommand;
17
26
  //# sourceMappingURL=doctor.d.ts.map
@@ -13,7 +13,8 @@ import { execSync, exec } from 'child_process';
13
13
  import { promisify } from 'util';
14
14
  import { decodeKey, isEncryptionEnabled } from '../encryption/vault.js';
15
15
  import { isEncryptedBlob } from '../encryption/vault.js';
16
- import { resolveMemoryPackageFromProject, readMemoryPackageVersion, recordMemoryPackagePath, } from '../init/memory-package-resolver.js';
16
+ import * as semver from 'semver';
17
+ import { resolveMemoryPackageFromProject, resolveMemoryPackageFromCli, readMemoryPackageVersion, recordMemoryPackagePath, } from '../init/memory-package-resolver.js';
17
18
  // Promisified exec with proper shell and env inheritance for cross-platform support
18
19
  const execAsync = promisify(exec);
19
20
  /**
@@ -1045,6 +1046,70 @@ async function checkLearningBridge() {
1045
1046
  fix: 'npm i -D @claude-flow/memory (optional dep appears absent — likely --omit=optional install)',
1046
1047
  };
1047
1048
  }
1049
+ /**
1050
+ * #3392: pure verdict for "does the @claude-flow/memory the CLI loads satisfy
1051
+ * the range the CLI declares?". `npx @claude-flow/cli@latest` reuses one npx
1052
+ * cache directory across CLI versions, and npm keeps an already-installed
1053
+ * dependency that still satisfies a caret range, so a stale memory could
1054
+ * survive a CLI upgrade with no error. Exported for unit testing.
1055
+ */
1056
+ export function evaluateMemoryPackageVersion(declared, installed) {
1057
+ const NAME = '@claude-flow/memory version';
1058
+ if (!declared) {
1059
+ return { name: NAME, status: 'warn', message: 'could not read the @claude-flow/memory range declared by @claude-flow/cli' };
1060
+ }
1061
+ if (!installed) {
1062
+ return {
1063
+ name: NAME,
1064
+ status: 'warn',
1065
+ message: `@claude-flow/memory is not resolvable from the CLI (declared ${declared}) — memory features fall back to degraded paths`,
1066
+ fix: `npm install @claude-flow/memory@${declared} --include=optional`,
1067
+ };
1068
+ }
1069
+ if (!semver.validRange(declared) || !semver.valid(installed)) {
1070
+ return { name: NAME, status: 'warn', message: `cannot compare installed ${installed} against declared ${declared}` };
1071
+ }
1072
+ if (semver.satisfies(installed, declared, { includePrerelease: true })) {
1073
+ return { name: NAME, status: 'pass', message: `v${installed} satisfies declared ${declared}` };
1074
+ }
1075
+ // warn, not fail: a dev/hoisted layout can legitimately differ, and doctor's
1076
+ // exit code must not depend on which copy a package manager happened to hoist.
1077
+ return {
1078
+ name: NAME,
1079
+ status: 'warn',
1080
+ message: `installed v${installed} does not satisfy declared ${declared} — a stale cached copy is running, so fixes shipped in a newer @claude-flow/memory are silently absent`,
1081
+ fix: `rm -rf "$(npm config get cache)/_npx" && npx @claude-flow/cli@latest doctor # or: npm install @claude-flow/memory@${declared}`,
1082
+ };
1083
+ }
1084
+ export async function checkMemoryPackageVersion() {
1085
+ try {
1086
+ // Walk up from this module to the CLI package root (npx cache, global,
1087
+ // project-local and monorepo dev all resolve the same way).
1088
+ let declared = null;
1089
+ let dir = dirname(fileURLToPath(import.meta.url));
1090
+ for (let i = 0; i < 8 && declared === null; i++) {
1091
+ const pj = join(dir, 'package.json');
1092
+ if (existsSync(pj)) {
1093
+ try {
1094
+ const pkg = JSON.parse(readFileSync(pj, 'utf-8'));
1095
+ if (pkg.name === '@claude-flow/cli') {
1096
+ declared = pkg.optionalDependencies?.['@claude-flow/memory'] ?? pkg.dependencies?.['@claude-flow/memory'] ?? null;
1097
+ break;
1098
+ }
1099
+ }
1100
+ catch { /* keep walking */ }
1101
+ }
1102
+ dir = dirname(dir);
1103
+ }
1104
+ // Resolve exactly as the CLI's own runtime does (its module context),
1105
+ // not from process.cwd() — that would report the project's copy instead.
1106
+ const distPath = resolveMemoryPackageFromCli();
1107
+ return evaluateMemoryPackageVersion(declared, distPath ? readMemoryPackageVersion(distPath) : null);
1108
+ }
1109
+ catch (err) {
1110
+ return { name: '@claude-flow/memory version', status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
1111
+ }
1112
+ }
1048
1113
  // Check API keys
1049
1114
  async function checkApiKeys() {
1050
1115
  const keys = ['ANTHROPIC_API_KEY', 'CLAUDE_API_KEY', 'OPENAI_API_KEY'];
@@ -2393,6 +2458,7 @@ export const doctorCommand = {
2393
2458
  checkMemoryStructuralIntegrity, // #2737 — bounded, native quick_check on every default run
2394
2459
  checkMemoryPersistenceDriver, // #2968/#3321 — read-only native capability probe
2395
2460
  checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
2461
+ checkMemoryPackageVersion, // #3392 — loaded @claude-flow/memory must satisfy the CLI's declared range
2396
2462
  checkApiKeys,
2397
2463
  checkMcpServers,
2398
2464
  checkMcpSchemaOverhead, // #2726 — fixed tools/list prompt cost
@@ -2430,11 +2496,13 @@ export const doctorCommand = {
2430
2496
  checkMemoryDatabase, // existing: exists + statable (unchanged)
2431
2497
  checkMemoryIntegrity, // #2677 check 1: sql.js open + PRAGMA integrity_check
2432
2498
  checkMemoryPersistenceDriver, // #2968/#3321: read-only native capability probe
2499
+ checkMemoryPackageVersion, // #3392: loaded memory package satisfies the declared range
2433
2500
  checkMemoryContent, // #2677 check 2: memory_entries content coverage
2434
2501
  checkMemoryEmbeddingCoverage, // #2677 check 3: vector coverage on populated rows
2435
2502
  checkMemoryReflexionCoverage, // #2677 check 6: episodes are retrievable
2436
2503
  checkMemoryCritiqueCoverage, // #2677 check 6: feedback carries lessons
2437
2504
  ],
2505
+ 'memory-package': checkMemoryPackageVersion, // #3392
2438
2506
  'learning': checkLearningBridge, // #2545
2439
2507
  'learning-bridge': checkLearningBridge, // #2545
2440
2508
  'api': checkApiKeys,
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.43.0",
3
+ "version": "3.45.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",
@@ -125,11 +125,11 @@
125
125
  "ws": "^8.21.0",
126
126
  "yaml": "^2.8.0",
127
127
  "zod": "^3.22.0",
128
- "@claude-flow/memory": "^3.0.0-alpha.23"
128
+ "@claude-flow/memory": "3.0.0-alpha.25"
129
129
  },
130
130
  "optionalDependencies": {
131
131
  "@agntcy/slim-bindings": "2.0.0-alpha.5",
132
- "@claude-flow/memory": "^3.0.0-alpha.23",
132
+ "@claude-flow/memory": "3.0.0-alpha.25",
133
133
  "@metaharness/darwin": "~0.10.2",
134
134
  "@metaharness/flywheel": "~0.1.10",
135
135
  "@metaharness/radio": "~0.1.0",