claude-flow 3.32.35 → 3.32.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.claude/helpers/hook-handler.cjs +134 -3
  2. package/.claude/helpers/intelligence.cjs +47 -4
  3. package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
  4. package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
  5. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
  6. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
  7. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
  8. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
  9. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
  10. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  11. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
  12. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  13. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
  14. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
  15. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
  16. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
  17. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  18. package/node_modules/@claude-flow/codex/dist/initializer.js +9 -13
  19. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  20. package/package.json +1 -1
  21. package/v3/@claude-flow/cli/bin/cli.js +25 -1
  22. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  23. package/v3/@claude-flow/cli/dist/src/commands/agent.js +1 -1
  24. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +159 -11
  25. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +39 -4
  26. package/v3/@claude-flow/cli/dist/src/commands/mcp.js +1 -1
  27. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +1 -0
  28. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +28 -0
  29. package/v3/@claude-flow/cli/dist/src/init/executor.js +16 -10
  30. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +15 -4
  31. package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -5
  32. package/v3/@claude-flow/cli/dist/src/mcp-server.d.ts +25 -0
  33. package/v3/@claude-flow/cli/dist/src/mcp-server.js +57 -2
  34. package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +51 -10
  35. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +34 -8
  36. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +2 -0
  37. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +5 -1
  38. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +1 -0
  39. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +81 -5
  40. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +1 -0
  41. package/v3/@claude-flow/cli/package.json +1 -1
@@ -50,6 +50,34 @@ function getSwarmStatus(swarmId) {
50
50
  // Ignore
51
51
  }
52
52
  }
53
+ // The canonical agent registry is the same source used by `agent list`.
54
+ // Prefer it over the swarm-level coordination boolean so idle agents are
55
+ // not reported as active (#2808). Hive agents are merged additively.
56
+ if (totalAgents === 0) {
57
+ try {
58
+ const canonicalPath = path.join(process.cwd(), '.claude-flow', 'agents', 'store.json');
59
+ const hivePath = path.join(process.cwd(), '.claude-flow', 'agents.json');
60
+ const merged = {};
61
+ for (const storePath of [hivePath, canonicalPath]) {
62
+ if (!fs.existsSync(storePath))
63
+ continue;
64
+ const parsed = JSON.parse(fs.readFileSync(storePath, 'utf-8'));
65
+ if (parsed?.agents && typeof parsed.agents === 'object') {
66
+ Object.assign(merged, parsed.agents);
67
+ }
68
+ }
69
+ const agents = Object.values(merged).filter(agent => agent.status !== 'terminated');
70
+ if (agents.length > 0) {
71
+ totalAgents = agents.length;
72
+ activeAgents = agents.filter(agent => agent.status === 'active' ||
73
+ agent.status === 'running' ||
74
+ agent.status === 'busy').length;
75
+ }
76
+ }
77
+ catch {
78
+ // Ignore — the count-only activity file remains the final fallback.
79
+ }
80
+ }
53
81
  // #2799 — `agent spawn` never writes `.swarm/agents/*.json`; it records
54
82
  // the count in `.claude-flow/metrics/swarm-activity.json` (via
55
83
  // updateSwarmActivityMetrics in commands/agent.ts). So when the agents
@@ -370,18 +370,19 @@ function mergeSettingsForUpgrade(existing) {
370
370
  // js/redos): a crafted settings.json command string with dozens of
371
371
  // dash-token repetitions can hang this check for minutes.
372
372
  const BROKEN_STATUSLINE_RE = /(?:npx\s+(?:--?\S+\s+){0,10})?@?claude-flow(?:\/cli)?(?:@\S+)?\s+hooks\s+statusline/;
373
+ const BROKEN_NPX_LATEST_RE = /npx\s+(?:--?\S+\s+){0,10}@?claude-flow\/cli@latest\s+\S+/;
373
374
  const existingStatusLine = existing.statusLine;
374
375
  if (existingStatusLine) {
375
376
  const existingCmd = typeof existingStatusLine.command === 'string' ? existingStatusLine.command : '';
376
- const isBroken = BROKEN_STATUSLINE_RE.test(existingCmd);
377
+ const isBroken = BROKEN_STATUSLINE_RE.test(existingCmd) || BROKEN_NPX_LATEST_RE.test(existingCmd);
377
378
  merged.statusLine = {
378
379
  type: 'command',
379
380
  command: isBroken || !existingCmd ? NEW_STATUSLINE_CMD : existingCmd,
380
381
  // Remove invalid fields: refreshMs, enabled (not supported by Claude Code)
381
382
  };
382
383
  }
383
- // #2448 — Detect and REGENERATE per-action hook commands that still use
384
- // the runaway `npx @claude-flow/cli@latest hooks <sub>` form. These fire
384
+ // #2448 / #2677 — Detect and REGENERATE per-action hook commands that still
385
+ // use the runaway `npx @claude-flow/cli@latest hooks <sub>` form. These fire
385
386
  // on every PreToolUse/PostToolUse/UserPromptSubmit, each spawning ~130 MB
386
387
  // of cold Node + npm registry resolution; the storm is what kernel-paniced
387
388
  // the reporter's machine in #2448.
@@ -409,15 +410,20 @@ function mergeSettingsForUpgrade(existing) {
409
410
  for (const group of groups) {
410
411
  if (!Array.isArray(group.hooks))
411
412
  continue;
412
- for (const h of group.hooks) {
413
+ group.hooks = group.hooks.filter((h) => {
413
414
  if (typeof h?.command !== 'string')
414
- continue;
415
+ return true;
415
416
  const m = BROKEN_HOOK_RE.exec(h.command);
416
- if (!m)
417
- continue;
418
- // Subcommand captured (e.g. "pre-bash", "post-edit", "route") — keep it.
419
- h.command = localHookCmd(m[1]);
420
- }
417
+ if (m) {
418
+ // Subcommand captured (e.g. "pre-bash", "post-edit", "route") — keep it.
419
+ h.command = localHookCmd(m[1]);
420
+ return true;
421
+ }
422
+ // Sibling CLI invocations (memory/daemon/swarm/...) have no
423
+ // hook-handler equivalent. Drop the stale extra; the merge above
424
+ // has already installed current local-helper hooks for this event.
425
+ return !BROKEN_NPX_LATEST_RE.test(h.command);
426
+ });
421
427
  }
422
428
  }
423
429
  }
@@ -775,11 +775,22 @@ export function generateIntelligenceStub() {
775
775
  "const path = require('path');",
776
776
  "const os = require('os');",
777
777
  '',
778
- "const DATA_DIR = path.join(process.cwd(), '.claude-flow', 'data');",
778
+ 'function resolveProjectRoot(startDir) {',
779
+ ' if (process.env.CLAUDE_PROJECT_DIR) return path.resolve(process.env.CLAUDE_PROJECT_DIR);',
780
+ ' var dir = path.resolve(startDir || process.cwd());',
781
+ ' while (true) {',
782
+ ' if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, ".claude-flow"))) return dir;',
783
+ ' var parent = path.dirname(dir);',
784
+ ' if (parent === dir) return path.resolve(startDir || process.cwd());',
785
+ ' dir = parent;',
786
+ ' }',
787
+ '}',
788
+ "const PROJECT_ROOT = resolveProjectRoot(process.cwd());",
789
+ "const DATA_DIR = path.join(PROJECT_ROOT, '.claude-flow', 'data');",
779
790
  "const STORE_PATH = path.join(DATA_DIR, 'auto-memory-store.json');",
780
791
  "const RANKED_PATH = path.join(DATA_DIR, 'ranked-context.json');",
781
792
  "const PENDING_PATH = path.join(DATA_DIR, 'pending-insights.jsonl');",
782
- "const SESSION_DIR = path.join(process.cwd(), '.claude-flow', 'sessions');",
793
+ "const SESSION_DIR = path.join(PROJECT_ROOT, '.claude-flow', 'sessions');",
783
794
  "const SESSION_FILE = path.join(SESSION_DIR, 'current.json');",
784
795
  '',
785
796
  'function ensureDir(dir) {',
@@ -823,8 +834,8 @@ export function generateIntelligenceStub() {
823
834
  ' var entries = [];',
824
835
  ' var candidates = [',
825
836
  ' path.join(os.homedir(), ".claude", "projects"),',
826
- ' path.join(process.cwd(), ".claude-flow", "memory"),',
827
- ' path.join(process.cwd(), ".claude", "memory"),',
837
+ ' path.join(PROJECT_ROOT, ".claude-flow", "memory"),',
838
+ ' path.join(PROJECT_ROOT, ".claude", "memory"),',
828
839
  ' ];',
829
840
  ' for (var i = 0; i < candidates.length; i++) {',
830
841
  ' try {',
@@ -53,6 +53,16 @@ function getInstalledCliVersionLocal() {
53
53
  return '0.0.0';
54
54
  }
55
55
  }
56
+ function compareVersionsLocal(a, b) {
57
+ const pa = a.split(/[.-]/).map(part => Number.parseInt(part, 10) || 0);
58
+ const pb = b.split(/[.-]/).map(part => Number.parseInt(part, 10) || 0);
59
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
60
+ const delta = (pa[i] ?? 0) - (pb[i] ?? 0);
61
+ if (delta !== 0)
62
+ return delta;
63
+ }
64
+ return 0;
65
+ }
56
66
  /**
57
67
  * Generate optimized statusline script
58
68
  * Output format:
@@ -91,10 +101,12 @@ export function generateStatuslineScript(options) {
91
101
  // getInstalledCliVersionLocal() above — reuse the pattern for the
92
102
  // same reason it exists there.
93
103
  let helperContent = null;
104
+ let helperPackageRoot = '';
94
105
  try {
95
106
  const esmRequire = createRequire(import.meta.url);
96
107
  const pkgJsonPath = esmRequire.resolve('@claude-flow/cli/package.json');
97
- const helperPath = path.join(path.dirname(pkgJsonPath), '.claude', 'helpers', 'statusline.cjs');
108
+ helperPackageRoot = path.dirname(pkgJsonPath);
109
+ const helperPath = path.join(helperPackageRoot, '.claude', 'helpers', 'statusline.cjs');
98
110
  helperContent = fs.readFileSync(helperPath, 'utf-8');
99
111
  }
100
112
  catch {
@@ -105,6 +117,7 @@ export function generateStatuslineScript(options) {
105
117
  if (pkg && pkg.name === '@claude-flow/cli') {
106
118
  const candidate = path.join(dir, '.claude', 'helpers', 'statusline.cjs');
107
119
  if (fs.existsSync(candidate)) {
120
+ helperPackageRoot = dir;
108
121
  helperContent = fs.readFileSync(candidate, 'utf-8');
109
122
  break;
110
123
  }
@@ -128,15 +141,15 @@ export function generateStatuslineScript(options) {
128
141
  // whatever the helper hard-codes) ships. Add a paired test in
129
142
  // statusline-cost-display.test.ts before changing either token.
130
143
  helperContent = helperContent.replace(/maxAgents: \d+,/, `maxAgents: ${maxAgents},`);
144
+ helperContent = helperContent.replace(/const BAKED_INSTALL_ROOT = "[^"]*";/, `const BAKED_INSTALL_ROOT = ${JSON.stringify(helperPackageRoot)};`);
131
145
  // Only overwrite the helper's baked version if OURS resolves higher.
132
146
  // Otherwise the substitution could DOWNGRADE (test environments where
133
147
  // esmRequire.resolve happens to hit an older node_modules install would
134
- // clobber a fresh committed helper). Naive lexicographic compare — works
135
- // for canonical semver strings with same-width digit parts, which is
136
- // reliable at this stage of the version space.
148
+ // clobber a fresh committed helper). Compare numeric components:
149
+ // lexicographic comparison freezes 3.32.24 below 3.32.8 (#2811).
137
150
  const helperVerMatch = helperContent.match(/let ver = "([^"]+)";/);
138
151
  const helperVer = helperVerMatch ? helperVerMatch[1] : '';
139
- if (!helperVer || bakedVersion > helperVer) {
152
+ if (!helperVer || compareVersionsLocal(bakedVersion, helperVer) > 0) {
140
153
  helperContent = helperContent.replace(/let ver = "[^"]+";/, `let ver = ${JSON.stringify(bakedVersion)};`);
141
154
  }
142
155
  return helperContent;
@@ -48,6 +48,31 @@ export interface MCPServerStatus {
48
48
  metrics?: Record<string, number>;
49
49
  };
50
50
  }
51
+ export declare function parseMcpToolSelection(value: string | undefined): string[] | 'all';
52
+ /**
53
+ * Apply the existing `--tools` contract to advertised schemas. A selector can
54
+ * be an exact tool name, a category, or a namespace prefix (`memory` matches
55
+ * `memory_store`). Execution remains registered internally; only the fixed
56
+ * per-request schema catalogue is reduced.
57
+ */
58
+ export declare function filterAdvertisedMcpTools<T extends {
59
+ name: string;
60
+ category?: string;
61
+ }>(tools: T[], selection: string[] | 'all'): T[];
62
+ export interface McpSchemaOverhead {
63
+ toolCount: number;
64
+ bytes: number;
65
+ estimatedTokens: number;
66
+ contextWindowTokens?: number;
67
+ ratio?: number;
68
+ risk: 'normal' | 'high';
69
+ }
70
+ /** Conservative JSON-size estimate for the fixed tools/list catalogue. */
71
+ export declare function assessMcpSchemaOverhead(tools: Array<{
72
+ name: string;
73
+ description?: string;
74
+ inputSchema?: unknown;
75
+ }>, contextWindowTokens?: number): McpSchemaOverhead;
51
76
  /**
52
77
  * MCP Server Manager
53
78
  *
@@ -42,6 +42,54 @@ const DEFAULT_OPTIONS = {
42
42
  daemonize: false,
43
43
  timeout: 30000,
44
44
  };
45
+ export function parseMcpToolSelection(value) {
46
+ if (!value || value.trim().toLowerCase() === 'all')
47
+ return 'all';
48
+ const selectors = value.split(',').map((item) => item.trim()).filter(Boolean);
49
+ return selectors.length > 0 ? selectors : 'all';
50
+ }
51
+ /**
52
+ * Apply the existing `--tools` contract to advertised schemas. A selector can
53
+ * be an exact tool name, a category, or a namespace prefix (`memory` matches
54
+ * `memory_store`). Execution remains registered internally; only the fixed
55
+ * per-request schema catalogue is reduced.
56
+ */
57
+ export function filterAdvertisedMcpTools(tools, selection) {
58
+ if (selection === 'all')
59
+ return tools;
60
+ const selectors = new Set(selection.map((item) => item.toLowerCase()));
61
+ return tools.filter((tool) => {
62
+ const name = tool.name.toLowerCase();
63
+ const category = tool.category?.toLowerCase();
64
+ return selectors.has(name)
65
+ || (category !== undefined && selectors.has(category))
66
+ || Array.from(selectors).some((selector) => name.startsWith(`${selector}_`));
67
+ });
68
+ }
69
+ /** Conservative JSON-size estimate for the fixed tools/list catalogue. */
70
+ export function assessMcpSchemaOverhead(tools, contextWindowTokens) {
71
+ const catalogue = tools.map((tool) => ({
72
+ name: tool.name,
73
+ description: tool.description,
74
+ inputSchema: tool.inputSchema,
75
+ }));
76
+ const bytes = Buffer.byteLength(JSON.stringify(catalogue), 'utf8');
77
+ const estimatedTokens = Math.ceil(bytes / 4);
78
+ const validWindow = Number.isFinite(contextWindowTokens) && Number(contextWindowTokens) > 0
79
+ ? Number(contextWindowTokens)
80
+ : undefined;
81
+ const ratio = validWindow ? estimatedTokens / validWindow : undefined;
82
+ return {
83
+ toolCount: tools.length,
84
+ bytes,
85
+ estimatedTokens,
86
+ ...(validWindow === undefined ? {} : { contextWindowTokens: validWindow }),
87
+ ...(ratio === undefined ? {} : { ratio }),
88
+ risk: (ratio !== undefined ? ratio >= 0.2 : estimatedTokens >= 8_000)
89
+ ? 'high'
90
+ : 'normal',
91
+ };
92
+ }
45
93
  /**
46
94
  * MCP Server Manager
47
95
  *
@@ -55,7 +103,14 @@ export class MCPServerManager extends EventEmitter {
55
103
  healthCheckInterval;
56
104
  constructor(options = {}) {
57
105
  super();
58
- this.options = { ...DEFAULT_OPTIONS, ...options };
106
+ // `options.tools`, populated by the `mcp start --tools` CLI flag, is
107
+ // spread last below and therefore takes precedence over this env fallback.
108
+ const environmentTools = parseMcpToolSelection(process.env.CLAUDE_FLOW_MCP_TOOLS);
109
+ this.options = {
110
+ ...DEFAULT_OPTIONS,
111
+ ...(environmentTools === 'all' ? {} : { tools: environmentTools }),
112
+ ...options,
113
+ };
59
114
  }
60
115
  /**
61
116
  * Start the MCP server
@@ -420,7 +475,7 @@ export class MCPServerManager extends EventEmitter {
420
475
  },
421
476
  };
422
477
  case 'tools/list':
423
- const tools = listMCPTools();
478
+ const tools = filterAdvertisedMcpTools(listMCPTools(), this.options.tools);
424
479
  return {
425
480
  jsonrpc: '2.0',
426
481
  id: message.id,
@@ -50,13 +50,16 @@ async function getRealEmbeddingFunction() {
50
50
  }
51
51
  return realEmbeddingFn;
52
52
  }
53
- // Generate real ONNX embedding (falls back to deterministic hash if ONNX unavailable)
54
53
  async function generateRealEmbedding(text, dimension) {
55
54
  const realFn = await getRealEmbeddingFunction();
56
55
  if (realFn) {
57
56
  try {
58
57
  const result = await realFn(text);
59
- return result.embedding;
58
+ return {
59
+ embedding: result.embedding,
60
+ backend: result.backend ?? 'onnx',
61
+ model: result.model,
62
+ };
60
63
  }
61
64
  catch {
62
65
  // Fall through to fallback
@@ -75,7 +78,11 @@ async function generateRealEmbedding(text, dimension) {
75
78
  }
76
79
  // L2 normalize
77
80
  const norm = Math.sqrt(embedding.reduce((sum, x) => sum + x * x, 0));
78
- return embedding.map(x => x / norm);
81
+ return {
82
+ embedding: embedding.map(x => x / norm),
83
+ backend: 'mock',
84
+ model: 'hash-fallback',
85
+ };
79
86
  }
80
87
  // Convert Euclidean embedding to Poincaré ball
81
88
  function toPoincare(euclidean, curvature) {
@@ -238,7 +245,8 @@ export const embeddingsTools = [
238
245
  }
239
246
  const useHyperbolic = input.hyperbolic === true && config.hyperbolic.enabled;
240
247
  // Generate real ONNX embedding
241
- const embedding = await generateRealEmbedding(text, config.dimension);
248
+ const generated = await generateRealEmbedding(text, config.dimension);
249
+ const embedding = generated.embedding;
242
250
  let result;
243
251
  let geometry;
244
252
  if (useHyperbolic) {
@@ -254,6 +262,8 @@ export const embeddingsTools = [
254
262
  embedding: result,
255
263
  metadata: {
256
264
  model: config.model,
265
+ embeddingBackend: generated.backend,
266
+ semanticGrounded: generated.backend === 'onnx',
257
267
  dimension: config.dimension,
258
268
  geometry,
259
269
  curvature: useHyperbolic ? config.hyperbolic.curvature : null,
@@ -309,10 +319,13 @@ export const embeddingsTools = [
309
319
  return { success: false, error: v.error };
310
320
  }
311
321
  // Generate real ONNX embeddings for both texts
312
- const [emb1, emb2] = await Promise.all([
322
+ const [generated1, generated2] = await Promise.all([
313
323
  generateRealEmbedding(text1, config.dimension),
314
324
  generateRealEmbedding(text2, config.dimension)
315
325
  ]);
326
+ const emb1 = generated1.embedding;
327
+ const emb2 = generated2.embedding;
328
+ const embeddingBackend = generated1.backend === 'onnx' && generated2.backend === 'onnx' ? 'onnx' : 'mock';
316
329
  let similarity;
317
330
  let distance;
318
331
  switch (metric) {
@@ -341,14 +354,21 @@ export const embeddingsTools = [
341
354
  similarity,
342
355
  distance,
343
356
  metric,
357
+ embeddingBackend,
358
+ semanticGrounded: embeddingBackend === 'onnx',
344
359
  texts: {
345
360
  text1: { length: text1.length, preview: text1.slice(0, 50) },
346
361
  text2: { length: text2.length, preview: text2.slice(0, 50) },
347
362
  },
348
- interpretation: similarity > 0.8 ? 'very similar' :
349
- similarity > 0.6 ? 'similar' :
350
- similarity > 0.4 ? 'somewhat similar' :
351
- similarity > 0.2 ? 'different' : 'very different',
363
+ interpretation: embeddingBackend === 'onnx'
364
+ ? similarity > 0.8 ? 'very similar'
365
+ : similarity > 0.6 ? 'similar'
366
+ : similarity > 0.4 ? 'somewhat similar'
367
+ : similarity > 0.2 ? 'different' : 'very different'
368
+ : null,
369
+ warning: embeddingBackend === 'mock'
370
+ ? 'Hash fallback scores are deterministic but not semantically meaningful.'
371
+ : undefined,
352
372
  };
353
373
  },
354
374
  },
@@ -426,6 +446,8 @@ export const embeddingsTools = [
426
446
  })),
427
447
  metadata: {
428
448
  model: config.model,
449
+ embeddingBackend: queryEmbedding.backend,
450
+ semanticGrounded: queryEmbedding.backend === 'onnx',
429
451
  topK,
430
452
  threshold,
431
453
  namespace: namespace || 'all',
@@ -433,6 +455,9 @@ export const embeddingsTools = [
433
455
  indexType: config.hyperbolic.enabled ? 'HNSW (hyperbolic)' : 'HNSW (euclidean)',
434
456
  resultCount: searchResult.results.length
435
457
  },
458
+ warning: queryEmbedding.backend === 'mock'
459
+ ? 'Results use a hash fallback and are not semantically ranked.'
460
+ : undefined,
436
461
  };
437
462
  }
438
463
  catch {
@@ -444,12 +469,17 @@ export const embeddingsTools = [
444
469
  results: [],
445
470
  metadata: {
446
471
  model: config.model,
472
+ embeddingBackend: queryEmbedding.backend,
473
+ semanticGrounded: queryEmbedding.backend === 'onnx',
447
474
  topK,
448
475
  threshold,
449
476
  namespace: namespace || 'all',
450
477
  searchTime: `${searchTime}ms`,
451
478
  indexType: config.hyperbolic.enabled ? 'HNSW (hyperbolic)' : 'HNSW (euclidean)',
452
479
  },
480
+ warning: queryEmbedding.backend === 'mock'
481
+ ? 'Hash fallback is active; semantic search is unavailable.'
482
+ : undefined,
453
483
  message: 'No embeddings indexed yet. Use memory store to add documents.',
454
484
  };
455
485
  }
@@ -795,6 +825,8 @@ export const embeddingsTools = [
795
825
  }
796
826
  catch { /* not installed */ }
797
827
  const ruvectorEnabled = config.neural.ruvector?.enabled ?? false;
828
+ const backendProbe = await generateRealEmbedding('ruflo embedding backend probe', config.dimension);
829
+ const semanticGrounded = backendProbe.backend === 'onnx';
798
830
  return {
799
831
  success: true,
800
832
  initialized: true,
@@ -822,11 +854,20 @@ export const embeddingsTools = [
822
854
  models: config.modelPath,
823
855
  },
824
856
  initializedAt: config.initialized,
857
+ embeddingBackend: backendProbe.backend,
858
+ semanticGrounded,
859
+ warning: semanticGrounded
860
+ ? undefined
861
+ : 'Hash fallback is active. Similarity scores are deterministic but not semantically meaningful.',
825
862
  capabilities: {
826
863
  onnxModels: ['Xenova/all-MiniLM-L6-v2', 'Xenova/all-mpnet-base-v2'],
827
864
  geometries: ['euclidean', 'poincare'],
828
865
  normalizations: ['L2', 'L1', 'minmax', 'zscore'],
829
- features: ['semantic search', 'hyperbolic projection', 'neural substrate'],
866
+ features: [
867
+ ...(semanticGrounded ? ['semantic search'] : []),
868
+ 'hyperbolic projection',
869
+ 'neural substrate',
870
+ ],
830
871
  },
831
872
  };
832
873
  },
@@ -439,6 +439,20 @@ function getIntelligenceStatsFromMemory() {
439
439
  const category = e.metadata?.category || 'general';
440
440
  categories[category] = (categories[category] || 0) + 1;
441
441
  });
442
+ const successfulPatterns = patternEntries.filter(e => e.metadata?.success === true).length;
443
+ const failedPatterns = patternEntries.filter(e => e.metadata?.success === false).length;
444
+ const describedPatterns = patternEntries.filter(e => {
445
+ if (typeof e.metadata?.task === 'string' && e.metadata.task.trim())
446
+ return true;
447
+ try {
448
+ const value = typeof e.value === 'string' ? JSON.parse(e.value) : e.value;
449
+ return typeof value?.task === 'string' &&
450
+ Boolean(String(value.task).trim());
451
+ }
452
+ catch {
453
+ return false;
454
+ }
455
+ }).length;
442
456
  // Count routing decisions
443
457
  const routingEntries = entries.filter(e => e.key.includes('routing') ||
444
458
  e.metadata?.type === 'routing-decision');
@@ -472,6 +486,10 @@ function getIntelligenceStatsFromMemory() {
472
486
  },
473
487
  patterns: {
474
488
  learned: patternEntries.length,
489
+ successful: successfulPatterns,
490
+ failed: failedPatterns,
491
+ unknown: Math.max(0, patternEntries.length - successfulPatterns - failedPatterns),
492
+ described: describedPatterns,
475
493
  categories,
476
494
  },
477
495
  memory: {
@@ -1074,21 +1092,28 @@ export const hooksMetrics = {
1074
1092
  agentCounts[o.agent] = (agentCounts[o.agent] || 0) + 1;
1075
1093
  }
1076
1094
  const topAgent = Object.entries(agentCounts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
1077
- const successful = stats.trajectories.successful;
1078
- const total = stats.trajectories.total;
1079
- const failed = Math.max(0, total - successful);
1080
1095
  return {
1081
1096
  _real: true,
1082
1097
  _dataSource: 'intelligence-stats + routing-outcomes',
1083
1098
  period,
1084
1099
  patterns: {
1085
1100
  total: stats.patterns.learned,
1086
- successful,
1087
- failed,
1101
+ successful: stats.patterns.successful,
1102
+ failed: stats.patterns.failed,
1103
+ unknown: stats.patterns.unknown,
1104
+ described: stats.patterns.described,
1105
+ descriptionCoverage: stats.patterns.learned > 0
1106
+ ? stats.patterns.described / stats.patterns.learned
1107
+ : null,
1088
1108
  avgConfidence: stats.routing.avgConfidence || null,
1089
1109
  },
1090
1110
  agents: {
1091
- routingAccuracy: stats.routing.avgConfidence || null,
1111
+ // Confidence is a model self-score, not observed routing accuracy
1112
+ // (#2809). Keep the old key as an explicit null for compatibility
1113
+ // while exposing the truthful additive field.
1114
+ routingAccuracy: null,
1115
+ averageConfidence: stats.routing.avgConfidence || null,
1116
+ outcomeSuccessRate: successRate,
1092
1117
  totalRoutes: stats.routing.decisions,
1093
1118
  topAgent,
1094
1119
  },
@@ -1097,7 +1122,7 @@ export const hooksMetrics = {
1097
1122
  successRate,
1098
1123
  avgRiskScore: null,
1099
1124
  },
1100
- _note: total === 0 && totalCommands === 0
1125
+ _note: stats.patterns.learned === 0 && totalCommands === 0
1101
1126
  ? 'No metrics data collected yet. Run hooks_post-task / hooks_intelligence_trajectory-end / hooks_route to populate.'
1102
1127
  : undefined,
1103
1128
  lastUpdated: new Date().toISOString(),
@@ -1311,6 +1336,7 @@ export const hooksPostTask = {
1311
1336
  const bridge = await import('../memory/memory-bridge.js');
1312
1337
  feedbackResult = await bridge.bridgeRecordFeedback({
1313
1338
  taskId,
1339
+ task: params.task || undefined,
1314
1340
  success,
1315
1341
  quality,
1316
1342
  agent,
@@ -3136,7 +3162,7 @@ export const hooksIntelligenceStats = {
3136
3162
  catch {
3137
3163
  memoryStats = {
3138
3164
  trajectories: { total: 0, successful: 0 },
3139
- patterns: { learned: 0, categories: {} },
3165
+ patterns: { learned: 0, successful: 0, failed: 0, unknown: 0, described: 0, categories: {} },
3140
3166
  memory: { indexSize: 0, totalAccessCount: 0, memorySizeBytes: 0 },
3141
3167
  routing: { decisions: 0, avgConfidence: 0 },
3142
3168
  };
@@ -322,6 +322,8 @@ export declare function bridgeSearchPatterns(options: {
322
322
  */
323
323
  export declare function bridgeRecordFeedback(options: {
324
324
  taskId: string;
325
+ /** Human-readable task text used as the semantic learning signal. */
326
+ task?: string;
325
327
  success: boolean;
326
328
  quality: number;
327
329
  agent?: string;
@@ -1840,11 +1840,15 @@ export async function bridgeRecordFeedback(options) {
1840
1840
  try {
1841
1841
  const intelligence = await import('./intelligence.js');
1842
1842
  const verdict = options.success ? 'success' : 'failure';
1843
+ const taskContext = options.task?.trim();
1843
1844
  const recorded = await intelligence.recordTrajectory([{
1844
1845
  type: 'action',
1845
- content: `Task ${options.taskId} completed by ${options.agent || 'unknown'} — success=${options.success}, quality=${options.quality.toFixed(3)}`,
1846
+ // Put the task description first so the embedding represents the
1847
+ // work, not just the bookkeeping suffix (#2812).
1848
+ content: `${taskContext ? `${taskContext} — ` : ''}Task ${options.taskId} completed by ${options.agent || 'unknown'} — success=${options.success}, quality=${options.quality.toFixed(3)}`,
1846
1849
  metadata: {
1847
1850
  taskId: options.taskId,
1851
+ task: taskContext,
1848
1852
  agent: options.agent,
1849
1853
  quality: options.quality,
1850
1854
  duration: options.duration,
@@ -20,6 +20,7 @@ export interface DistillOptions {
20
20
  export interface DistillReport {
21
21
  processed: number;
22
22
  episodes: number;
23
+ episodeEmbeddings: number;
23
24
  patterns: number;
24
25
  patternEmbeddings: number;
25
26
  causalEdges: number;