@claude-flow/cli 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 (48) 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 +2 -2
  7. package/dist/src/commands/agent.js +1 -1
  8. package/dist/src/commands/doctor.js +159 -11
  9. package/dist/src/commands/hooks.js +39 -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/swarm.js +28 -0
  13. package/dist/src/init/executor.js +16 -10
  14. package/dist/src/init/helpers-generator.js +15 -4
  15. package/dist/src/init/statusline-generator.js +18 -5
  16. package/dist/src/mcp-server.d.ts +25 -0
  17. package/dist/src/mcp-server.js +57 -2
  18. package/dist/src/mcp-tools/embeddings-tools.js +51 -10
  19. package/dist/src/mcp-tools/hooks-tools.js +34 -8
  20. package/dist/src/memory/memory-bridge.d.ts +2 -0
  21. package/dist/src/memory/memory-bridge.js +5 -1
  22. package/dist/src/services/memory-distillation.d.ts +1 -0
  23. package/dist/src/services/memory-distillation.js +81 -5
  24. package/dist/src/services/worker-daemon.js +1 -0
  25. package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
  26. package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
  27. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
  28. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
  29. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
  30. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
  31. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
  32. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  33. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
  34. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  35. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
  36. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
  37. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
  38. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
  39. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  40. package/node_modules/@claude-flow/codex/dist/initializer.js +9 -13
  41. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  42. package/package.json +1 -1
  43. package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
  44. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
  45. package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
  46. package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
  47. package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
  48. package/plugins/ruflo-metaharness/scripts/threat-model.mjs +4 -1
@@ -120,8 +120,8 @@ async function checkConfigFile() {
120
120
  }
121
121
  // Check daemon status
122
122
  /**
123
- * #2448 — Detect the runaway `npx @claude-flow/cli@latest` statusLine / hook
124
- * commands left over in `.claude/settings.json` from pre-#2337 installs.
123
+ * #2448 / #2677 — Detect runaway `npx @claude-flow/cli@latest` commands
124
+ * left over in `.claude/settings.json` from pre-#2337 installs.
125
125
  *
126
126
  * These fire on every Claude Code event (statusLine refires every few hundred
127
127
  * ms, hooks fire per tool-use), each spawning a cold Node process + npm
@@ -138,7 +138,9 @@ async function checkStaleSettingsNpx() {
138
138
  // Same regex pattern the executor migration uses — kept in sync. Flag-list
139
139
  // repetition bounded at 10 (CodeQL js/redos — unbounded `*` here is
140
140
  // exponential-backtracking-prone on a crafted settings.json).
141
- const BROKEN_RE = /npx\s+(?:--?\S+\s+){0,10}@?claude-flow\/cli@latest\s+hooks\s+(?:statusline|\S+)/;
141
+ // Every subcommand incurs the same cold npm process/registry cost. The
142
+ // previous `hooks` literal missed `memory`, `daemon`, `swarm`, etc.
143
+ const BROKEN_RE = /npx\s+(?:--?\S+\s+){0,10}@?claude-flow\/cli@latest\s+\S+/;
142
144
  // Look in both project-local and home-dir settings.
143
145
  const candidates = [
144
146
  join(process.cwd(), '.claude', 'settings.json'),
@@ -263,18 +265,21 @@ async function checkMemoryDatabase() {
263
265
  // The existing `checkMemoryDatabase` above asserts existence + statability
264
266
  // only, so it CANNOT distinguish a healthy DB from a 99.97%-empty or
265
267
  // SQLite-malformed one. Stuinfla reported both cases live (81-store fleet).
266
- // The three checks below layer functional assertions on top, ordered so
268
+ // The checks below layer functional assertions on top, ordered so
267
269
  // the earliest chain-break is always the first red the user sees:
268
270
  // 1. Integrity — can sql.js open it AND does PRAGMA integrity_check pass?
269
271
  // 2. Content — do most memory_entries rows carry non-empty content?
270
272
  // 3. Embedding coverage — do most rows have a vector? (unembedded rows are
271
273
  // both unrecallable AND undistillable per ADR-174)
274
+ // 6. Reflexion coverage — can episodes participate in retrieveRelevant's
275
+ // required episode_embeddings INNER JOIN?
276
+ // Feedback critiques — do execution-tier episodes carry a real lesson?
272
277
  // Ordering matters: content ratio is meaningless on a DB that can't open;
273
278
  // embedding coverage is meaningless on rows with no content. First red wins.
274
279
  //
275
- // Recall probe (stuinfla check 4) requires actual write+search+delete round
276
- // trips through the CLI's own memory pipeline deferred to a follow-up PR
277
- // to keep this one purely additive and safe.
280
+ // Recall probe (stuinfla check 4) still requires an isolated write+search+
281
+ // delete round trip through the CLI's own memory pipeline. It remains separate
282
+ // because doctor otherwise stays read-only.
278
283
  //
279
284
  // Design rules (also from stuinfla's report):
280
285
  // - "A check that cannot fail protects nothing" — every check has a
@@ -731,6 +736,102 @@ async function checkMemoryEmbeddingCoverage() {
731
736
  catch { /* best-effort */ }
732
737
  }
733
738
  }
739
+ // #2677 check 6 — ReflexionMemory.retrieveRelevant() INNER JOINs episodes to
740
+ // episode_embeddings. A populated episodes table with an empty/missing partner
741
+ // table is therefore not degraded recall; it is structurally zero recall.
742
+ async function checkMemoryReflexionCoverage() {
743
+ const dbPath = await resolveMemoryDbPath();
744
+ if (!dbPath)
745
+ return { name: 'Memory Reflexion Coverage', status: 'warn', message: 'no memory.db found' };
746
+ const db = await tryOpenSqlJs(dbPath);
747
+ if (!db)
748
+ return { name: 'Memory Reflexion Coverage', status: 'warn', message: 'DB unreadable (see Memory Integrity)' };
749
+ try {
750
+ const tables = new Set(db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('episodes','episode_embeddings')")[0]?.values?.map((v) => String(v[0])) ?? []);
751
+ if (!tables.has('episodes')) {
752
+ return { name: 'Memory Reflexion Coverage', status: 'warn', message: 'episodes table absent — AgentDB Reflexion schema not initialized' };
753
+ }
754
+ const episodes = Number(db.exec('SELECT count(*) FROM episodes')[0]?.values?.[0]?.[0] ?? 0);
755
+ if (episodes === 0) {
756
+ return { name: 'Memory Reflexion Coverage', status: 'warn', message: `${dbPath} — 0 episodes (Reflexion has not been exercised)` };
757
+ }
758
+ if (!tables.has('episode_embeddings')) {
759
+ return {
760
+ name: 'Memory Reflexion Coverage',
761
+ status: 'fail',
762
+ message: `${dbPath} — episode embeddings 0/${episodes} (0.00%); retrieveRelevant() cannot return rows`,
763
+ fix: 'run `claude-flow memory distill run`; current distillation creates/backfills episode_embeddings',
764
+ };
765
+ }
766
+ const embedded = Number(db.exec('SELECT count(*) FROM episodes e JOIN episode_embeddings ee ON ee.episode_id=e.id')[0]?.values?.[0]?.[0] ?? 0);
767
+ const ratio = embedded / episodes;
768
+ const detail = `episode embeddings ${embedded}/${episodes} (${(ratio * 100).toFixed(2)}%)`;
769
+ if (ratio < 0.95) {
770
+ return {
771
+ name: 'Memory Reflexion Coverage',
772
+ status: 'fail',
773
+ message: `${dbPath} — ${detail} below 95% floor; retrieveRelevant() cannot see uncovered episodes`,
774
+ fix: 'run `claude-flow memory distill run` to populate retrievable episodes',
775
+ };
776
+ }
777
+ return { name: 'Memory Reflexion Coverage', status: 'pass', message: `${dbPath} — ${detail}` };
778
+ }
779
+ catch (e) {
780
+ return { name: 'Memory Reflexion Coverage', status: 'warn', message: `${dbPath} — probe threw: ${e.message || String(e)}` };
781
+ }
782
+ finally {
783
+ try {
784
+ db.close();
785
+ }
786
+ catch { /* best-effort */ }
787
+ }
788
+ }
789
+ // Execution-tier feedback should carry a lesson, not just reward/success bits.
790
+ // Proxy episodes intentionally remain critique-free to avoid laundering a
791
+ // structural summary into an observed failure explanation.
792
+ async function checkMemoryCritiqueCoverage() {
793
+ const dbPath = await resolveMemoryDbPath();
794
+ if (!dbPath)
795
+ return { name: 'Memory Feedback Critiques', status: 'warn', message: 'no memory.db found' };
796
+ const db = await tryOpenSqlJs(dbPath);
797
+ if (!db)
798
+ return { name: 'Memory Feedback Critiques', status: 'warn', message: 'DB unreadable (see Memory Integrity)' };
799
+ try {
800
+ const hasEpisodes = (db.exec("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='episodes'")[0]?.values?.[0]?.[0] ?? 0) > 0;
801
+ if (!hasEpisodes)
802
+ return { name: 'Memory Feedback Critiques', status: 'warn', message: 'episodes table absent' };
803
+ const row = db.exec(`
804
+ SELECT count(*),
805
+ sum(CASE WHEN length(trim(coalesce(critique,''))) > 0 THEN 1 ELSE 0 END)
806
+ FROM episodes WHERE session_id LIKE 'distill:feedback%'
807
+ `)[0]?.values?.[0] ?? [0, 0];
808
+ const feedback = Number(row[0] ?? 0);
809
+ const withCritique = Number(row[1] ?? 0);
810
+ if (feedback === 0) {
811
+ return { name: 'Memory Feedback Critiques', status: 'warn', message: `${dbPath} — 0 execution-feedback episodes to assess` };
812
+ }
813
+ const ratio = withCritique / feedback;
814
+ const detail = `feedback critiques ${withCritique}/${feedback} (${(ratio * 100).toFixed(2)}%)`;
815
+ if (ratio < 0.95) {
816
+ return {
817
+ name: 'Memory Feedback Critiques',
818
+ status: 'fail',
819
+ message: `${dbPath} — ${detail} below 95% floor`,
820
+ fix: 'run current `claude-flow memory distill run`; execution-tier episodes preserve observed critique/error text',
821
+ };
822
+ }
823
+ return { name: 'Memory Feedback Critiques', status: 'pass', message: `${dbPath} — ${detail}` };
824
+ }
825
+ catch (e) {
826
+ return { name: 'Memory Feedback Critiques', status: 'warn', message: `${dbPath} — probe threw: ${e.message || String(e)}` };
827
+ }
828
+ finally {
829
+ try {
830
+ db.close();
831
+ }
832
+ catch { /* best-effort */ }
833
+ }
834
+ }
734
835
  // #2545: Check that the self-learning bridge can actually load @claude-flow/memory
735
836
  // the SAME way the SessionStart auto-memory hook does. On the documented `npx ruflo`
736
837
  // path the package lands in the npx cache — unreachable from the project — so the
@@ -1027,6 +1128,48 @@ async function checkMcpServers() {
1027
1128
  fix: 'claude mcp add ruflo -- npx -y ruflo@latest mcp start',
1028
1129
  };
1029
1130
  }
1131
+ // #2726 — tools/list is fixed prompt overhead on clients/backends that do not
1132
+ // defer MCP schemas. Measure the actual live registry and warn before a small
1133
+ // context window becomes unrecoverable even after compaction.
1134
+ async function checkMcpSchemaOverhead() {
1135
+ try {
1136
+ const [{ listMCPTools }, { assessMcpSchemaOverhead, filterAdvertisedMcpTools, parseMcpToolSelection, }] = await Promise.all([
1137
+ import('../mcp-client.js'),
1138
+ import('../mcp-server.js'),
1139
+ ]);
1140
+ // The `mcp start --tools` CLI flag takes precedence when the server is
1141
+ // constructed. Doctor intentionally inspects the environment fallback so
1142
+ // its estimate describes the configuration inherited by MCP clients.
1143
+ const selection = parseMcpToolSelection(process.env.CLAUDE_FLOW_MCP_TOOLS);
1144
+ const tools = filterAdvertisedMcpTools(listMCPTools(), selection);
1145
+ // This is diagnostic metadata about the external client's context window,
1146
+ // not Ruflo command configuration, so there is no corresponding CLI flag.
1147
+ const rawWindow = process.env.CLAUDE_FLOW_CONTEXT_WINDOW_TOKENS;
1148
+ const contextWindow = rawWindow ? Number.parseInt(rawWindow, 10) : undefined;
1149
+ const assessment = assessMcpSchemaOverhead(tools, contextWindow);
1150
+ const ratio = assessment.ratio === undefined
1151
+ ? ''
1152
+ : `, ${(assessment.ratio * 100).toFixed(1)}% of ${assessment.contextWindowTokens}-token window`;
1153
+ const selectionLabel = selection === 'all' ? 'all tools' : `filtered: ${selection.join(',')}`;
1154
+ const message = `${assessment.toolCount} advertised tools ≈ ${assessment.estimatedTokens} schema tokens (${selectionLabel}${ratio})`;
1155
+ if (assessment.risk === 'high') {
1156
+ return {
1157
+ name: 'MCP Schema Overhead',
1158
+ status: 'warn',
1159
+ message,
1160
+ fix: 'Set CLAUDE_FLOW_MCP_TOOLS to required categories/tool names (for example: memory,swarm,agent,hooks) and optionally CLAUDE_FLOW_CONTEXT_WINDOW_TOKENS to your backend limit.',
1161
+ };
1162
+ }
1163
+ return { name: 'MCP Schema Overhead', status: 'pass', message };
1164
+ }
1165
+ catch (error) {
1166
+ return {
1167
+ name: 'MCP Schema Overhead',
1168
+ status: 'warn',
1169
+ message: `Unable to measure: ${error instanceof Error ? error.message : String(error)}`,
1170
+ };
1171
+ }
1172
+ }
1030
1173
  // Check disk space (async with proper env inheritance)
1031
1174
  async function checkDiskSpace() {
1032
1175
  try {
@@ -1843,7 +1986,7 @@ export const doctorCommand = {
1843
1986
  {
1844
1987
  name: 'component',
1845
1988
  short: 'c',
1846
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, metaharness)',
1989
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, mcp-overhead, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, metaharness)',
1847
1990
  type: 'string'
1848
1991
  },
1849
1992
  {
@@ -1958,13 +2101,14 @@ export const doctorCommand = {
1958
2101
  checkGit,
1959
2102
  checkGitRepo,
1960
2103
  checkConfigFile,
1961
- checkStaleSettingsNpx, // #2448 — runaway `npx @latest` in statusLine/hooks
2104
+ checkStaleSettingsNpx, // #2448/#2677 — runaway `npx @latest` in settings
1962
2105
  checkDaemonStatus,
1963
2106
  checkMemoryDatabase,
1964
2107
  checkMemoryStructuralIntegrity, // #2737 — bounded, native quick_check on every default run
1965
2108
  checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
1966
2109
  checkApiKeys,
1967
2110
  checkMcpServers,
2111
+ checkMcpSchemaOverhead, // #2726 — fixed tools/list prompt cost
1968
2112
  checkAIDefence, // #1807
1969
2113
  checkDiskSpace,
1970
2114
  checkBuildTools,
@@ -1982,8 +2126,9 @@ export const doctorCommand = {
1982
2126
  // array — expanded at execution time. Stuinfla's report showed the
1983
2127
  // existence-only check reporting PASS on a 99.97%-empty and even a
1984
2128
  // SQLite-malformed DB; the array here layers integrity → content →
1985
- // embedding coverage over the existing existence probe, ordered so
1986
- // the earliest chain-break is always the first red the user sees.
2129
+ // embedding coverage over the existing existence probe; check 6 then
2130
+ // verifies that distilled episodes are actually Reflexion-retrievable and
2131
+ // execution feedback carries a lesson.
1987
2132
  const componentMap = {
1988
2133
  'version': checkVersionFreshness,
1989
2134
  'freshness': checkVersionFreshness,
@@ -1998,12 +2143,15 @@ export const doctorCommand = {
1998
2143
  checkMemoryIntegrity, // #2677 check 1: sql.js open + PRAGMA integrity_check
1999
2144
  checkMemoryContent, // #2677 check 2: memory_entries content coverage
2000
2145
  checkMemoryEmbeddingCoverage, // #2677 check 3: vector coverage on populated rows
2146
+ checkMemoryReflexionCoverage, // #2677 check 6: episodes are retrievable
2147
+ checkMemoryCritiqueCoverage, // #2677 check 6: feedback carries lessons
2001
2148
  ],
2002
2149
  'learning': checkLearningBridge, // #2545
2003
2150
  'learning-bridge': checkLearningBridge, // #2545
2004
2151
  'api': checkApiKeys,
2005
2152
  'git': checkGit,
2006
2153
  'mcp': checkMcpServers,
2154
+ 'mcp-overhead': checkMcpSchemaOverhead,
2007
2155
  'aidefence': checkAIDefence, // #1807
2008
2156
  'disk': checkDiskSpace,
2009
2157
  'typescript': checkBuildTools,
@@ -1218,8 +1218,20 @@ const metricsCommand = {
1218
1218
  const totalPatterns = safeNum(rawMetrics.patterns?.total ?? rawMetrics.summary?.patternsLearned);
1219
1219
  const successfulPatterns = safeNum(rawMetrics.patterns?.successful ?? Math.round(safeNum(rawMetrics.summary?.successRate) * totalPatterns));
1220
1220
  const failedPatterns = Math.max(0, safeNum(rawMetrics.patterns?.failed ?? totalPatterns - successfulPatterns));
1221
+ const unknownPatterns = Math.max(0, safeNum(rawMetrics.patterns?.unknown));
1222
+ const describedPatterns = Math.max(0, safeNum(rawMetrics.patterns?.described));
1223
+ const descriptionCoverage = safeNum(rawMetrics.patterns?.descriptionCoverage ??
1224
+ (totalPatterns > 0 ? describedPatterns / totalPatterns : 0));
1221
1225
  const avgConfidence = safeNum(rawMetrics.patterns?.avgConfidence ?? rawMetrics.summary?.avgQuality);
1222
- const routingAccuracy = safeNum(rawMetrics.agents?.routingAccuracy ?? rawMetrics.routing?.avgConfidence);
1226
+ const routingAccuracy = rawMetrics.agents?.routingAccuracy == null
1227
+ ? null
1228
+ : safeNum(rawMetrics.agents.routingAccuracy);
1229
+ const averageRoutingConfidence = safeNum(rawMetrics.agents?.averageConfidence ??
1230
+ rawMetrics.routing?.avgConfidence ??
1231
+ rawMetrics.patterns?.avgConfidence);
1232
+ const routingOutcomeSuccessRate = rawMetrics.agents?.outcomeSuccessRate == null
1233
+ ? null
1234
+ : safeNum(rawMetrics.agents.outcomeSuccessRate);
1223
1235
  const totalRoutes = safeNum(rawMetrics.agents?.totalRoutes ?? rawMetrics.routing?.totalRoutes);
1224
1236
  const topAgent = rawMetrics.agents?.topAgent ?? rawMetrics.routing?.topAgents?.[0]?.agent ?? 'n/a';
1225
1237
  const totalCommands = safeNum(rawMetrics.commands?.totalExecuted ?? rawMetrics.commands?.totalCommands);
@@ -1227,8 +1239,22 @@ const metricsCommand = {
1227
1239
  const avgRiskScore = safeNum(rawMetrics.commands?.avgRiskScore ?? rawMetrics.commands?.avgExecutionTime);
1228
1240
  const result = {
1229
1241
  ...rawMetrics,
1230
- patterns: { total: totalPatterns, successful: successfulPatterns, failed: failedPatterns, avgConfidence },
1231
- agents: { routingAccuracy, totalRoutes, topAgent },
1242
+ patterns: {
1243
+ total: totalPatterns,
1244
+ successful: successfulPatterns,
1245
+ failed: failedPatterns,
1246
+ unknown: unknownPatterns,
1247
+ described: describedPatterns,
1248
+ descriptionCoverage,
1249
+ avgConfidence,
1250
+ },
1251
+ agents: {
1252
+ routingAccuracy,
1253
+ averageConfidence: averageRoutingConfidence,
1254
+ outcomeSuccessRate: routingOutcomeSuccessRate,
1255
+ totalRoutes,
1256
+ topAgent,
1257
+ },
1232
1258
  commands: { totalExecuted: totalCommands, successRate: commandSuccessRate, avgRiskScore },
1233
1259
  };
1234
1260
  if (ctx.flags.format === 'json') {
@@ -1246,6 +1272,8 @@ const metricsCommand = {
1246
1272
  { metric: 'Total Patterns', value: totalPatterns },
1247
1273
  { metric: 'Successful', value: output.success(String(successfulPatterns)) },
1248
1274
  { metric: 'Failed', value: output.error(String(failedPatterns)) },
1275
+ ...(unknownPatterns > 0 ? [{ metric: 'Unclassified', value: String(unknownPatterns) }] : []),
1276
+ { metric: 'With Task Context', value: `${(descriptionCoverage * 100).toFixed(1)}%` },
1249
1277
  { metric: 'Avg Confidence', value: `${(avgConfidence * 100).toFixed(1)}%` }
1250
1278
  ]
1251
1279
  });
@@ -1258,7 +1286,14 @@ const metricsCommand = {
1258
1286
  { key: 'value', header: 'Value', width: 20, align: 'right' }
1259
1287
  ],
1260
1288
  data: [
1261
- { metric: 'Routing Accuracy', value: `${(routingAccuracy * 100).toFixed(1)}%` },
1289
+ {
1290
+ metric: routingAccuracy === null ? 'Avg Confidence' : 'Routing Accuracy',
1291
+ value: `${((routingAccuracy ?? averageRoutingConfidence) * 100).toFixed(1)}%`,
1292
+ },
1293
+ ...(routingOutcomeSuccessRate === null ? [] : [{
1294
+ metric: 'Outcome Success Rate',
1295
+ value: `${(routingOutcomeSuccessRate * 100).toFixed(1)}%`,
1296
+ }]),
1262
1297
  { metric: 'Total Routes', value: totalRoutes },
1263
1298
  { metric: 'Top Agent', value: output.highlight(topAgent) }
1264
1299
  ]
@@ -63,7 +63,7 @@ const startCommand = {
63
63
  },
64
64
  {
65
65
  name: 'tools',
66
- description: 'Tools to enable (comma-separated or "all")',
66
+ description: 'Tools to advertise (comma-separated categories, prefixes, exact names, or "all")',
67
67
  type: 'string',
68
68
  default: 'all'
69
69
  },
@@ -182,6 +182,7 @@ const runCommand = {
182
182
  data: [
183
183
  { metric: 'Processed', value: String(report.processed) },
184
184
  { metric: 'Episodes', value: String(report.episodes) },
185
+ { metric: 'Episode Embeddings', value: String(report.episodeEmbeddings) },
185
186
  { metric: 'Patterns', value: String(report.patterns) },
186
187
  { metric: 'Pattern Embeddings', value: String(report.patternEmbeddings) },
187
188
  { metric: 'Causal Edges', value: String(report.causalEdges) },
@@ -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,