@claude-flow/cli 3.32.34 → 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 (71) hide show
  1. package/.claude/helpers/helpers.manifest.json +5 -5
  2. package/.claude/helpers/hook-handler.cjs +30 -0
  3. package/.claude/helpers/intelligence.cjs +47 -4
  4. package/.claude/helpers/statusline.cjs +169 -6
  5. package/bin/cli.js +25 -1
  6. package/catalog-manifest.json +4 -4
  7. package/dist/src/commands/agent.js +50 -2
  8. package/dist/src/commands/doctor.js +159 -11
  9. package/dist/src/commands/hooks.js +63 -4
  10. package/dist/src/commands/mcp.js +1 -1
  11. package/dist/src/commands/memory-distill.js +1 -0
  12. package/dist/src/commands/metaharness.js +4 -0
  13. package/dist/src/commands/swarm.js +89 -5
  14. package/dist/src/config-adapter.js +1 -0
  15. package/dist/src/index.js +3 -2
  16. package/dist/src/init/executor.js +16 -10
  17. package/dist/src/init/helpers-generator.js +15 -4
  18. package/dist/src/init/statusline-generator.js +18 -5
  19. package/dist/src/mcp-server.d.ts +25 -0
  20. package/dist/src/mcp-server.js +57 -2
  21. package/dist/src/mcp-tools/agent-tools.js +10 -0
  22. package/dist/src/mcp-tools/embeddings-tools.js +51 -10
  23. package/dist/src/mcp-tools/hooks-tools.js +120 -60
  24. package/dist/src/mcp-tools/memory-tools.js +14 -2
  25. package/dist/src/mcp-tools/metaharness-tools.js +7 -0
  26. package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
  27. package/dist/src/mcp-tools/swarm-tools.js +172 -7
  28. package/dist/src/memory/memory-bridge.d.ts +2 -0
  29. package/dist/src/memory/memory-bridge.js +5 -1
  30. package/dist/src/memory/memory-initializer.d.ts +9 -1
  31. package/dist/src/memory/memory-initializer.js +66 -19
  32. package/dist/src/services/daemon-autostart.js +7 -4
  33. package/dist/src/services/flywheel-receipt.d.ts +3 -0
  34. package/dist/src/services/flywheel-receipt.js +1 -0
  35. package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
  36. package/dist/src/services/harness-flywheel-runtime.js +19 -19
  37. package/dist/src/services/harness-flywheel.d.ts +2 -0
  38. package/dist/src/services/harness-flywheel.js +1 -0
  39. package/dist/src/services/harness-project-anchor.d.ts +23 -0
  40. package/dist/src/services/harness-project-anchor.js +129 -0
  41. package/dist/src/services/learned-routing.d.ts +34 -0
  42. package/dist/src/services/learned-routing.js +85 -0
  43. package/dist/src/services/memory-distillation.d.ts +1 -0
  44. package/dist/src/services/memory-distillation.js +81 -5
  45. package/dist/src/services/pheromone-adaptive.d.ts +71 -0
  46. package/dist/src/services/pheromone-adaptive.js +214 -0
  47. package/dist/src/services/worker-daemon.js +5 -1
  48. package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
  49. package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
  50. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
  51. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
  52. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
  53. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
  54. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
  55. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  56. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
  57. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  58. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
  59. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
  60. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
  61. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
  62. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  63. package/node_modules/@claude-flow/codex/dist/initializer.js +9 -13
  64. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  65. package/package.json +1 -1
  66. package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
  67. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
  68. package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
  69. package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
  70. package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
  71. package/plugins/ruflo-metaharness/scripts/threat-model.mjs +4 -1
@@ -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,
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { getProjectCwd } from './types.js';
10
10
  import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
11
11
  import { executeAgentTask } from './agent-execute-core.js';
12
+ import { pheromoneAgentEligibility } from './swarm-tools.js';
12
13
  // Storage paths
13
14
  const STORAGE_DIR = '.claude-flow';
14
15
  const AGENT_DIR = 'agents';
@@ -383,6 +384,15 @@ export const agentTools = [
383
384
  const vP = validateText(input.prompt, 'prompt');
384
385
  if (!vP.valid)
385
386
  return { success: false, error: `Input validation failed: ${vP.error}` };
387
+ const pheromone = pheromoneAgentEligibility(input.agentId);
388
+ if (!pheromone.eligible) {
389
+ return {
390
+ success: false,
391
+ agentId: input.agentId,
392
+ suspended: true,
393
+ error: pheromone.reason,
394
+ };
395
+ }
386
396
  // Delegate to the shared core (also used by the workflow runtime).
387
397
  return executeAgentTask({
388
398
  agentId: input.agentId,
@@ -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
  },