@claude-flow/cli 3.42.5 → 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.
- package/.claude/helpers/helpers.manifest.json +4 -3
- package/.claude/helpers/router.js +1 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/doctor.js +22 -1
- package/dist/src/commands/hooks.js +9 -3
- package/dist/src/init/executor.js +2 -1
- package/dist/src/init/helper-refresh.js +5 -0
- package/dist/src/init/helpers-generator.js +11 -10
- package/dist/src/mcp-tools/capability-brain.js +4 -2
- package/dist/src/mcp-tools/hooks-tools.d.ts +36 -0
- package/dist/src/mcp-tools/hooks-tools.js +291 -189
- package/dist/src/mcp-tools/memory-tools.js +42 -0
- package/dist/src/memory/graph-edge-writer.d.ts +10 -0
- package/dist/src/memory/graph-edge-writer.js +77 -1
- package/dist/src/memory/memory-bridge.d.ts +6 -0
- package/dist/src/memory/memory-bridge.js +76 -28
- package/dist/src/memory/memory-initializer.d.ts +3 -0
- package/dist/src/memory/memory-initializer.js +53 -11
- package/dist/src/ruvector/router-embedder.d.ts +58 -0
- package/dist/src/ruvector/router-embedder.js +141 -0
- package/dist/src/ruvector/typesafe-router.d.ts +119 -0
- package/dist/src/ruvector/typesafe-router.js +245 -0
- package/dist/src/services/policy-runtime.js +40 -11
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/package.json +5 -1
|
@@ -9,6 +9,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
11
|
import { buildLearnedRoutingPatterns, } from '../services/learned-routing.js';
|
|
12
|
+
import { applyTypesafeRouting, getTypesafeRouter } from '../ruvector/typesafe-router.js';
|
|
13
|
+
import { DEFAULT_ROUTER_EMBEDDER, embedForRouter, resolveRouterEmbedder, } from '../ruvector/router-embedder.js';
|
|
12
14
|
// Real vector search functions - lazy loaded to avoid circular imports
|
|
13
15
|
let searchEntriesFn = null;
|
|
14
16
|
/**
|
|
@@ -107,46 +109,16 @@ async function getMoERouter() {
|
|
|
107
109
|
// Tries native VectorDb first (16k+ routes/s HNSW), falls back to pure JS (47k routes/s cosine)
|
|
108
110
|
let semanticRouter = null;
|
|
109
111
|
let nativeVectorDb = null;
|
|
110
|
-
let semanticRouterInitialized = false;
|
|
111
112
|
let routerBackend = 'none';
|
|
112
113
|
// Pre-computed embeddings for common task patterns (cached)
|
|
113
114
|
const TASK_PATTERN_EMBEDDINGS = new Map();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
for (let i = 0; i < dimension; i++) {
|
|
122
|
-
let value = 0;
|
|
123
|
-
// Word-level features
|
|
124
|
-
for (let w = 0; w < words.length; w++) {
|
|
125
|
-
const word = words[w];
|
|
126
|
-
for (let c = 0; c < word.length; c++) {
|
|
127
|
-
const charCode = word.charCodeAt(c);
|
|
128
|
-
value += Math.sin((charCode * (i + 1) + w * 17 + c * 23) * 0.0137);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// Character-level features
|
|
132
|
-
for (let c = 0; c < text.length; c++) {
|
|
133
|
-
value += Math.cos((text.charCodeAt(c) * (i + 1) + c * 7) * 0.0073);
|
|
134
|
-
}
|
|
135
|
-
embedding[i] = value / Math.max(1, text.length);
|
|
136
|
-
}
|
|
137
|
-
// Normalize
|
|
138
|
-
let norm = 0;
|
|
139
|
-
for (let i = 0; i < dimension; i++) {
|
|
140
|
-
norm += embedding[i] * embedding[i];
|
|
141
|
-
}
|
|
142
|
-
norm = Math.sqrt(norm);
|
|
143
|
-
if (norm > 0) {
|
|
144
|
-
for (let i = 0; i < dimension; i++) {
|
|
145
|
-
embedding[i] /= norm;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return embedding;
|
|
149
|
-
}
|
|
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;
|
|
150
122
|
// ── Runtime routing outcome persistence ──────────────────────────────
|
|
151
123
|
// Closes the learning loop: post-task records outcomes → route loads them.
|
|
152
124
|
const ROUTING_OUTCOMES_PATH = join(resolve('.'), '.claude-flow/routing-outcomes.json');
|
|
@@ -190,7 +162,8 @@ function saveRoutingOutcomes(outcomes) {
|
|
|
190
162
|
// The prior singleton cache made the learned store inert until restart.
|
|
191
163
|
semanticRouter = null;
|
|
192
164
|
nativeVectorDb = null;
|
|
193
|
-
|
|
165
|
+
routerInitPromise = null;
|
|
166
|
+
routerRequestedEmbedder = null;
|
|
194
167
|
routerBackend = 'none';
|
|
195
168
|
TASK_PATTERN_EMBEDDINGS.clear();
|
|
196
169
|
}
|
|
@@ -269,15 +242,65 @@ const TASK_PATTERNS = {
|
|
|
269
242
|
agents: ['memory-specialist', 'architect', 'coder'],
|
|
270
243
|
},
|
|
271
244
|
};
|
|
245
|
+
/** Wrap hooks_route so the opt-in typesafe router (src/ruvector/typesafe-router.ts) can override the legacy pick. */
|
|
246
|
+
function withTypesafeRouting(legacy) {
|
|
247
|
+
return async (params) => applyTypesafeRouting(params, (await legacy(params)), TASK_PATTERNS, getTypesafeRouter());
|
|
248
|
+
}
|
|
272
249
|
/**
|
|
273
250
|
* Get the semantic router with environment detection.
|
|
274
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.
|
|
275
256
|
*/
|
|
276
|
-
async function getSemanticRouter() {
|
|
277
|
-
|
|
278
|
-
|
|
257
|
+
async function getSemanticRouter(requested) {
|
|
258
|
+
const selection = resolveRouterEmbedder(requested);
|
|
259
|
+
if (routerInitPromise && routerRequestedEmbedder === selection.kind) {
|
|
260
|
+
return routerInitPromise;
|
|
261
|
+
}
|
|
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;
|
|
279
289
|
}
|
|
280
|
-
|
|
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
|
+
});
|
|
281
304
|
// STEP 1: Try native VectorDb from @ruvector/router (HNSW-backed)
|
|
282
305
|
// Note: Native VectorDb uses a persistent database file which can have lock issues
|
|
283
306
|
// in concurrent environments. We try it first but fall back gracefully to pure JS.
|
|
@@ -299,17 +322,17 @@ async function getSemanticRouter() {
|
|
|
299
322
|
hnswEfSearch: 100,
|
|
300
323
|
});
|
|
301
324
|
// Initialize with static + runtime-learned task patterns
|
|
302
|
-
for (const [patternName, { keywords }] of Object.entries(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
db.insert(`${patternName}:${keyword}`,
|
|
306
|
-
TASK_PATTERN_EMBEDDINGS.set(`${patternName}:${keyword}`,
|
|
307
|
-
}
|
|
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
|
+
});
|
|
308
331
|
}
|
|
309
332
|
nativeVectorDb = db;
|
|
310
333
|
routerBackend = 'native';
|
|
311
|
-
console.log(
|
|
312
|
-
return
|
|
334
|
+
console.log(`[hooks] Semantic router initialized: native VectorDb (HNSW, 16k+ routes/s), embedder=${routerEmbedder}`);
|
|
335
|
+
return handle();
|
|
313
336
|
}
|
|
314
337
|
}
|
|
315
338
|
catch (err) {
|
|
@@ -321,8 +344,8 @@ async function getSemanticRouter() {
|
|
|
321
344
|
try {
|
|
322
345
|
const { SemanticRouter } = await import('../ruvector/semantic-router.js');
|
|
323
346
|
semanticRouter = new SemanticRouter({ dimension: 384 });
|
|
324
|
-
for (const [patternName, { keywords, agents, source, support, reliability }] of Object.entries(
|
|
325
|
-
const embeddings =
|
|
347
|
+
for (const [patternName, { keywords, agents, source, support, reliability }] of Object.entries(patterns)) {
|
|
348
|
+
const embeddings = embedded.vectors.get(patternName) ?? [];
|
|
326
349
|
semanticRouter.addIntentWithEmbeddings(patternName, embeddings, {
|
|
327
350
|
agents,
|
|
328
351
|
keywords,
|
|
@@ -336,14 +359,14 @@ async function getSemanticRouter() {
|
|
|
336
359
|
});
|
|
337
360
|
}
|
|
338
361
|
routerBackend = 'pure-js';
|
|
339
|
-
console.log(
|
|
362
|
+
console.log(`[hooks] Semantic router initialized: pure JS (cosine, 47k routes/s), embedder=${routerEmbedder}`);
|
|
340
363
|
}
|
|
341
364
|
catch {
|
|
342
365
|
semanticRouter = null;
|
|
343
366
|
routerBackend = 'none';
|
|
344
367
|
console.log('[hooks] Semantic router initialized: none (no backend available)');
|
|
345
368
|
}
|
|
346
|
-
return
|
|
369
|
+
return handle();
|
|
347
370
|
}
|
|
348
371
|
/**
|
|
349
372
|
* Get router backend info for status display.
|
|
@@ -621,11 +644,21 @@ function suggestAgentsForFile(filePath) {
|
|
|
621
644
|
}
|
|
622
645
|
return AGENT_PATTERNS[ext] || ['coder', 'architect'];
|
|
623
646
|
}
|
|
624
|
-
|
|
625
|
-
|
|
647
|
+
// Whole-word matchers for KEYWORD_PATTERNS. A bare `includes()` matched
|
|
648
|
+
// substrings: 'test' hit "latest" (tester @ 0.95), 'auth' hit "author",
|
|
649
|
+
// 'fix' hit "prefix", 'api' hit "capitalize". Single words get \b anchors plus
|
|
650
|
+
// simple inflections (tests, testing, fixes, deployed); phrases containing
|
|
651
|
+
// whitespace or '/' (e.g. 'ci/cd') match literally between word boundaries.
|
|
652
|
+
const KEYWORD_MATCHERS = Object.entries(KEYWORD_PATTERNS).map(([keyword, result]) => {
|
|
653
|
+
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
654
|
+
const body = /[\s/]/.test(keyword) ? escaped : `${escaped}(?:s|es|ing|ed)?`;
|
|
655
|
+
return { regex: new RegExp(`\\b${body}\\b`, 'i'), result };
|
|
656
|
+
});
|
|
657
|
+
/** Exported for tests. */
|
|
658
|
+
export function suggestAgentsForTask(task) {
|
|
626
659
|
// Check static keyword patterns first
|
|
627
|
-
for (const
|
|
628
|
-
if (
|
|
660
|
+
for (const { regex, result } of KEYWORD_MATCHERS) {
|
|
661
|
+
if (regex.test(task)) {
|
|
629
662
|
return result;
|
|
630
663
|
}
|
|
631
664
|
}
|
|
@@ -950,7 +983,8 @@ export const hooksRoute = {
|
|
|
950
983
|
},
|
|
951
984
|
required: ['task'],
|
|
952
985
|
},
|
|
953
|
-
|
|
986
|
+
// Opt-in @ruvector/typesafe augmentation (CLAUDE_FLOW_ROUTER_TYPESAFE=1); returns the legacy result unchanged when unset.
|
|
987
|
+
handler: withTypesafeRouting(async (params) => {
|
|
954
988
|
const task = params.task;
|
|
955
989
|
const context = params.context;
|
|
956
990
|
const useSemanticRouter = params.useSemanticRouter !== false;
|
|
@@ -1005,134 +1039,185 @@ export const hooksRoute = {
|
|
|
1005
1039
|
// AgentDB router not available — fall through to local routing
|
|
1006
1040
|
}
|
|
1007
1041
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const routeStart = performance.now();
|
|
1051
|
-
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);
|
|
1052
1084
|
routingLatencyMs = performance.now() - routeStart;
|
|
1053
|
-
routingMethod = 'semantic-
|
|
1054
|
-
backendInfo = '
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (!learned)
|
|
1072
|
-
return true;
|
|
1073
|
-
return Number(match.metadata.support ?? 0) >= 2
|
|
1074
|
-
&& Number(match.metadata.reliability ?? 0) >= 0.75;
|
|
1075
|
-
});
|
|
1076
|
-
if (eligibleSemantic) {
|
|
1077
|
-
const topMatch = eligibleSemantic;
|
|
1078
|
-
agents = topMatch.metadata.agents || ['coder', 'researcher'];
|
|
1079
|
-
confidence = topMatch.score;
|
|
1080
|
-
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
|
+
});
|
|
1081
1103
|
}
|
|
1082
|
-
|
|
1083
|
-
//
|
|
1084
|
-
const suggestion = suggestAgentsForTask(task);
|
|
1085
|
-
agents = suggestion.agents;
|
|
1086
|
-
confidence = suggestion.confidence;
|
|
1087
|
-
matchedPattern = 'keyword-fallback';
|
|
1088
|
-
routingMethod = 'keyword';
|
|
1089
|
-
backendInfo = 'keyword matching';
|
|
1104
|
+
catch {
|
|
1105
|
+
// Native failed, try pure JS fallback
|
|
1090
1106
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
|
|
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
|
+
}
|
|
1136
1221
|
export const hooksMetrics = {
|
|
1137
1222
|
name: 'hooks_metrics',
|
|
1138
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.',
|
|
@@ -1439,10 +1524,12 @@ export const hooksPostTask = {
|
|
|
1439
1524
|
catch {
|
|
1440
1525
|
// Non-fatal
|
|
1441
1526
|
}
|
|
1442
|
-
// Record trajectory via intelligence module (SONA + ReasoningBank)
|
|
1527
|
+
// Record trajectory via intelligence module (SONA + ReasoningBank).
|
|
1528
|
+
// #3353: keep the observed result instead of discarding it.
|
|
1529
|
+
let trajectoryRecorded = false;
|
|
1443
1530
|
try {
|
|
1444
1531
|
const intelligence = await import('../memory/intelligence.js');
|
|
1445
|
-
await intelligence.recordTrajectory([{ type: 'result', content: params.task || taskId, metadata: { success, agent, quality }, timestamp: Date.now() }], success ? 'success' : 'failure');
|
|
1532
|
+
trajectoryRecorded = (await intelligence.recordTrajectory([{ type: 'result', content: params.task || taskId, metadata: { success, agent, quality }, timestamp: Date.now() }], success ? 'success' : 'failure')) === true;
|
|
1446
1533
|
}
|
|
1447
1534
|
catch {
|
|
1448
1535
|
// Intelligence module not available — non-fatal
|
|
@@ -1582,17 +1669,32 @@ export const hooksPostTask = {
|
|
|
1582
1669
|
writeFileSync(storePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
1583
1670
|
}
|
|
1584
1671
|
catch { /* non-critical */ }
|
|
1672
|
+
// #3353: report only observed learning results. The previous
|
|
1673
|
+
// `feedbackResult?.updated || (success ? 2 : 1)` / `newPatterns: success ? 1 : 0`
|
|
1674
|
+
// invented counts whenever the feedback controller was unavailable (and the
|
|
1675
|
+
// `||` turned an observed 0 into 2). No path reports pattern *creation*, so
|
|
1676
|
+
// newPatterns is null (unknown) rather than a guess; the trajectory has no
|
|
1677
|
+
// real id to surface, so trajectoryId is null.
|
|
1678
|
+
const feedbackRecorded = feedbackResult?.success === true;
|
|
1679
|
+
const learningAvailable = feedbackRecorded;
|
|
1585
1680
|
return {
|
|
1586
1681
|
taskId,
|
|
1587
1682
|
success,
|
|
1588
1683
|
duration,
|
|
1589
1684
|
learningUpdates: {
|
|
1590
|
-
patternsUpdated: feedbackResult?.updated
|
|
1591
|
-
newPatterns:
|
|
1592
|
-
trajectoryId:
|
|
1685
|
+
patternsUpdated: feedbackRecorded ? (feedbackResult?.updated ?? 0) : 0,
|
|
1686
|
+
newPatterns: null,
|
|
1687
|
+
trajectoryId: null,
|
|
1593
1688
|
controller: feedbackResult?.controller || 'none',
|
|
1594
1689
|
outcomePersisted,
|
|
1690
|
+
available: learningAvailable,
|
|
1691
|
+
...(learningAvailable ? {} : {
|
|
1692
|
+
reason: feedbackResult
|
|
1693
|
+
? `feedback controller '${feedbackResult.controller}' did not record the outcome`
|
|
1694
|
+
: 'feedback controller unavailable',
|
|
1695
|
+
}),
|
|
1595
1696
|
},
|
|
1697
|
+
trajectory: { recorded: trajectoryRecorded },
|
|
1596
1698
|
quality,
|
|
1597
1699
|
pheromone,
|
|
1598
1700
|
feedback: feedbackResult ? {
|