@claude-flow/cli 3.32.1 → 3.32.3

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 (657) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +3 -3
  3. package/.claude/helpers/statusline.cjs +24 -17
  4. package/.claude/helpers/statusline.js +19 -31
  5. package/catalog-manifest.json +2 -2
  6. package/package.json +1 -1
  7. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  8. package/plugins/ruflo-metaharness/scripts/smoke.sh +8 -5
  9. package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +1 -1
  10. package/dist/src/agenticow/speculative-exploration.d.ts +0 -148
  11. package/dist/src/agenticow/speculative-exploration.js +0 -218
  12. package/dist/src/appliance/gguf-engine.d.ts +0 -91
  13. package/dist/src/appliance/gguf-engine.js +0 -425
  14. package/dist/src/appliance/ruvllm-bridge.d.ts +0 -102
  15. package/dist/src/appliance/ruvllm-bridge.js +0 -292
  16. package/dist/src/appliance/rvfa-builder.d.ts +0 -44
  17. package/dist/src/appliance/rvfa-builder.js +0 -329
  18. package/dist/src/appliance/rvfa-distribution.d.ts +0 -97
  19. package/dist/src/appliance/rvfa-distribution.js +0 -370
  20. package/dist/src/appliance/rvfa-format.d.ts +0 -111
  21. package/dist/src/appliance/rvfa-format.js +0 -393
  22. package/dist/src/appliance/rvfa-runner.d.ts +0 -69
  23. package/dist/src/appliance/rvfa-runner.js +0 -237
  24. package/dist/src/appliance/rvfa-signing.d.ts +0 -123
  25. package/dist/src/appliance/rvfa-signing.js +0 -347
  26. package/dist/src/autopilot-state.d.ts +0 -77
  27. package/dist/src/autopilot-state.js +0 -271
  28. package/dist/src/benchmarks/gaia-agent-planning.smoke.d.ts +0 -18
  29. package/dist/src/benchmarks/gaia-agent-planning.smoke.js +0 -253
  30. package/dist/src/benchmarks/gaia-agent.d.ts +0 -198
  31. package/dist/src/benchmarks/gaia-agent.js +0 -651
  32. package/dist/src/benchmarks/gaia-causal-memory.d.ts +0 -133
  33. package/dist/src/benchmarks/gaia-causal-memory.js +0 -281
  34. package/dist/src/benchmarks/gaia-causal-memory.smoke.d.ts +0 -22
  35. package/dist/src/benchmarks/gaia-causal-memory.smoke.js +0 -300
  36. package/dist/src/benchmarks/gaia-convergence.d.ts +0 -138
  37. package/dist/src/benchmarks/gaia-convergence.js +0 -260
  38. package/dist/src/benchmarks/gaia-convergence.smoke.d.ts +0 -19
  39. package/dist/src/benchmarks/gaia-convergence.smoke.js +0 -246
  40. package/dist/src/benchmarks/gaia-critic.d.ts +0 -123
  41. package/dist/src/benchmarks/gaia-critic.js +0 -312
  42. package/dist/src/benchmarks/gaia-critic.smoke.d.ts +0 -21
  43. package/dist/src/benchmarks/gaia-critic.smoke.js +0 -327
  44. package/dist/src/benchmarks/gaia-decomposer.d.ts +0 -125
  45. package/dist/src/benchmarks/gaia-decomposer.js +0 -350
  46. package/dist/src/benchmarks/gaia-decomposer.smoke.d.ts +0 -21
  47. package/dist/src/benchmarks/gaia-decomposer.smoke.js +0 -228
  48. package/dist/src/benchmarks/gaia-e2e-smoke.d.ts +0 -27
  49. package/dist/src/benchmarks/gaia-e2e-smoke.js +0 -136
  50. package/dist/src/benchmarks/gaia-extract.smoke.d.ts +0 -45
  51. package/dist/src/benchmarks/gaia-extract.smoke.js +0 -242
  52. package/dist/src/benchmarks/gaia-hardness/features.d.ts +0 -46
  53. package/dist/src/benchmarks/gaia-hardness/features.js +0 -170
  54. package/dist/src/benchmarks/gaia-hardness/predictor.d.ts +0 -105
  55. package/dist/src/benchmarks/gaia-hardness/predictor.js +0 -260
  56. package/dist/src/benchmarks/gaia-hardness/predictor.smoke.d.ts +0 -20
  57. package/dist/src/benchmarks/gaia-hardness/predictor.smoke.js +0 -235
  58. package/dist/src/benchmarks/gaia-hardness/train-data-loader.d.ts +0 -51
  59. package/dist/src/benchmarks/gaia-hardness/train-data-loader.js +0 -179
  60. package/dist/src/benchmarks/gaia-judge.d.ts +0 -88
  61. package/dist/src/benchmarks/gaia-judge.js +0 -437
  62. package/dist/src/benchmarks/gaia-loader.d.ts +0 -87
  63. package/dist/src/benchmarks/gaia-loader.js +0 -326
  64. package/dist/src/benchmarks/gaia-tools/file_read.d.ts +0 -35
  65. package/dist/src/benchmarks/gaia-tools/file_read.js +0 -403
  66. package/dist/src/benchmarks/gaia-tools/grounded_query.d.ts +0 -126
  67. package/dist/src/benchmarks/gaia-tools/grounded_query.js +0 -225
  68. package/dist/src/benchmarks/gaia-tools/index.d.ts +0 -32
  69. package/dist/src/benchmarks/gaia-tools/index.js +0 -36
  70. package/dist/src/benchmarks/gaia-tools/types.d.ts +0 -62
  71. package/dist/src/benchmarks/gaia-tools/types.js +0 -12
  72. package/dist/src/benchmarks/gaia-tools/web_search.d.ts +0 -30
  73. package/dist/src/benchmarks/gaia-tools/web_search.js +0 -210
  74. package/dist/src/benchmarks/gaia-voting.d.ts +0 -88
  75. package/dist/src/benchmarks/gaia-voting.js +0 -297
  76. package/dist/src/benchmarks/gaia-voting.smoke.d.ts +0 -20
  77. package/dist/src/benchmarks/gaia-voting.smoke.js +0 -332
  78. package/dist/src/benchmarks/pretrain/index.d.ts +0 -58
  79. package/dist/src/benchmarks/pretrain/index.js +0 -404
  80. package/dist/src/business-pods/bbs-budget-tracker.d.ts +0 -139
  81. package/dist/src/business-pods/bbs-budget-tracker.js +0 -358
  82. package/dist/src/business-pods/domain-affinity-policy.d.ts +0 -47
  83. package/dist/src/business-pods/domain-affinity-policy.js +0 -65
  84. package/dist/src/business-pods/pod-schema.d.ts +0 -96
  85. package/dist/src/business-pods/pod-schema.js +0 -225
  86. package/dist/src/commands/advisor.d.ts +0 -15
  87. package/dist/src/commands/advisor.js +0 -94
  88. package/dist/src/commands/agent-wasm.d.ts +0 -14
  89. package/dist/src/commands/agent-wasm.js +0 -333
  90. package/dist/src/commands/agent.d.ts +0 -8
  91. package/dist/src/commands/agent.js +0 -927
  92. package/dist/src/commands/analyze.d.ts +0 -19
  93. package/dist/src/commands/analyze.js +0 -2047
  94. package/dist/src/commands/announcements.d.ts +0 -17
  95. package/dist/src/commands/announcements.js +0 -0
  96. package/dist/src/commands/appliance-advanced.d.ts +0 -9
  97. package/dist/src/commands/appliance-advanced.js +0 -215
  98. package/dist/src/commands/appliance.d.ts +0 -8
  99. package/dist/src/commands/appliance.js +0 -404
  100. package/dist/src/commands/autopilot.d.ts +0 -15
  101. package/dist/src/commands/autopilot.js +0 -407
  102. package/dist/src/commands/benchmark.d.ts +0 -10
  103. package/dist/src/commands/benchmark.js +0 -460
  104. package/dist/src/commands/claims.d.ts +0 -10
  105. package/dist/src/commands/claims.js +0 -620
  106. package/dist/src/commands/cleanup.d.ts +0 -13
  107. package/dist/src/commands/cleanup.js +0 -250
  108. package/dist/src/commands/completions.d.ts +0 -10
  109. package/dist/src/commands/completions.js +0 -539
  110. package/dist/src/commands/config.d.ts +0 -8
  111. package/dist/src/commands/config.js +0 -428
  112. package/dist/src/commands/daemon.d.ts +0 -59
  113. package/dist/src/commands/daemon.js +0 -1733
  114. package/dist/src/commands/deployment.d.ts +0 -10
  115. package/dist/src/commands/deployment.js +0 -672
  116. package/dist/src/commands/doctor.d.ts +0 -10
  117. package/dist/src/commands/doctor.js +0 -1695
  118. package/dist/src/commands/eject.d.ts +0 -33
  119. package/dist/src/commands/eject.js +0 -195
  120. package/dist/src/commands/embeddings.d.ts +0 -18
  121. package/dist/src/commands/embeddings.js +0 -1623
  122. package/dist/src/commands/funnel.d.ts +0 -18
  123. package/dist/src/commands/funnel.js +0 -302
  124. package/dist/src/commands/gaia-bench.d.ts +0 -40
  125. package/dist/src/commands/gaia-bench.js +0 -597
  126. package/dist/src/commands/guidance.d.ts +0 -8
  127. package/dist/src/commands/guidance.js +0 -556
  128. package/dist/src/commands/hive-mind.d.ts +0 -11
  129. package/dist/src/commands/hive-mind.js +0 -1319
  130. package/dist/src/commands/hooks.d.ts +0 -8
  131. package/dist/src/commands/hooks.js +0 -4595
  132. package/dist/src/commands/index.d.ts +0 -118
  133. package/dist/src/commands/index.js +0 -364
  134. package/dist/src/commands/init.d.ts +0 -13
  135. package/dist/src/commands/init.js +0 -1297
  136. package/dist/src/commands/issues.d.ts +0 -21
  137. package/dist/src/commands/issues.js +0 -567
  138. package/dist/src/commands/mcp.d.ts +0 -11
  139. package/dist/src/commands/mcp.js +0 -732
  140. package/dist/src/commands/memory-backup.d.ts +0 -11
  141. package/dist/src/commands/memory-backup.js +0 -46
  142. package/dist/src/commands/memory-distill.d.ts +0 -27
  143. package/dist/src/commands/memory-distill.js +0 -374
  144. package/dist/src/commands/memory.d.ts +0 -8
  145. package/dist/src/commands/memory.js +0 -1596
  146. package/dist/src/commands/metaharness.d.ts +0 -39
  147. package/dist/src/commands/metaharness.js +0 -215
  148. package/dist/src/commands/migrate.d.ts +0 -8
  149. package/dist/src/commands/migrate.js +0 -742
  150. package/dist/src/commands/neural.d.ts +0 -10
  151. package/dist/src/commands/neural.js +0 -4331
  152. package/dist/src/commands/performance.d.ts +0 -10
  153. package/dist/src/commands/performance.js +0 -583
  154. package/dist/src/commands/plugins.d.ts +0 -11
  155. package/dist/src/commands/plugins.js +0 -826
  156. package/dist/src/commands/process.d.ts +0 -10
  157. package/dist/src/commands/process.js +0 -694
  158. package/dist/src/commands/progress.d.ts +0 -11
  159. package/dist/src/commands/progress.js +0 -259
  160. package/dist/src/commands/providers.d.ts +0 -10
  161. package/dist/src/commands/providers.js +0 -502
  162. package/dist/src/commands/proxy.d.ts +0 -21
  163. package/dist/src/commands/proxy.js +0 -310
  164. package/dist/src/commands/route.d.ts +0 -16
  165. package/dist/src/commands/route.js +0 -822
  166. package/dist/src/commands/ruvector/backup.d.ts +0 -11
  167. package/dist/src/commands/ruvector/backup.js +0 -747
  168. package/dist/src/commands/ruvector/benchmark.d.ts +0 -11
  169. package/dist/src/commands/ruvector/benchmark.js +0 -490
  170. package/dist/src/commands/ruvector/import.d.ts +0 -18
  171. package/dist/src/commands/ruvector/import.js +0 -373
  172. package/dist/src/commands/ruvector/index.d.ts +0 -29
  173. package/dist/src/commands/ruvector/index.js +0 -129
  174. package/dist/src/commands/ruvector/init.d.ts +0 -11
  175. package/dist/src/commands/ruvector/init.js +0 -467
  176. package/dist/src/commands/ruvector/migrate.d.ts +0 -11
  177. package/dist/src/commands/ruvector/migrate.js +0 -498
  178. package/dist/src/commands/ruvector/optimize.d.ts +0 -11
  179. package/dist/src/commands/ruvector/optimize.js +0 -505
  180. package/dist/src/commands/ruvector/pg-utils.d.ts +0 -14
  181. package/dist/src/commands/ruvector/pg-utils.js +0 -41
  182. package/dist/src/commands/ruvector/setup.d.ts +0 -18
  183. package/dist/src/commands/ruvector/setup.js +0 -765
  184. package/dist/src/commands/ruvector/status.d.ts +0 -11
  185. package/dist/src/commands/ruvector/status.js +0 -479
  186. package/dist/src/commands/security.d.ts +0 -10
  187. package/dist/src/commands/security.js +0 -1007
  188. package/dist/src/commands/session.d.ts +0 -8
  189. package/dist/src/commands/session.js +0 -757
  190. package/dist/src/commands/settings.d.ts +0 -19
  191. package/dist/src/commands/settings.js +0 -180
  192. package/dist/src/commands/spinner.d.ts +0 -16
  193. package/dist/src/commands/spinner.js +0 -329
  194. package/dist/src/commands/start.d.ts +0 -8
  195. package/dist/src/commands/start.js +0 -418
  196. package/dist/src/commands/status.d.ts +0 -8
  197. package/dist/src/commands/status.js +0 -608
  198. package/dist/src/commands/swarm.d.ts +0 -8
  199. package/dist/src/commands/swarm.js +0 -891
  200. package/dist/src/commands/task.d.ts +0 -8
  201. package/dist/src/commands/task.js +0 -675
  202. package/dist/src/commands/transfer-store.d.ts +0 -13
  203. package/dist/src/commands/transfer-store.js +0 -428
  204. package/dist/src/commands/update.d.ts +0 -8
  205. package/dist/src/commands/update.js +0 -276
  206. package/dist/src/commands/verify.d.ts +0 -19
  207. package/dist/src/commands/verify.js +0 -261
  208. package/dist/src/commands/version.d.ts +0 -42
  209. package/dist/src/commands/version.js +0 -106
  210. package/dist/src/commands/workflow.d.ts +0 -8
  211. package/dist/src/commands/workflow.js +0 -617
  212. package/dist/src/config/harness-feedback-applier.d.ts +0 -50
  213. package/dist/src/config/harness-feedback-applier.js +0 -122
  214. package/dist/src/config/proven-config-refresh.d.ts +0 -39
  215. package/dist/src/config/proven-config-refresh.js +0 -154
  216. package/dist/src/config/proven-config-rvfa.d.ts +0 -23
  217. package/dist/src/config/proven-config-rvfa.js +0 -73
  218. package/dist/src/config/proven-config.d.ts +0 -86
  219. package/dist/src/config/proven-config.js +0 -176
  220. package/dist/src/config-adapter.d.ts +0 -15
  221. package/dist/src/config-adapter.js +0 -186
  222. package/dist/src/encryption/vault.d.ts +0 -94
  223. package/dist/src/encryption/vault.js +0 -172
  224. package/dist/src/fs-secure.d.ts +0 -86
  225. package/dist/src/fs-secure.js +0 -133
  226. package/dist/src/funnel/advisor-tip.d.ts +0 -58
  227. package/dist/src/funnel/advisor-tip.js +0 -92
  228. package/dist/src/funnel/attribution.d.ts +0 -37
  229. package/dist/src/funnel/attribution.js +0 -101
  230. package/dist/src/funnel/consent.d.ts +0 -22
  231. package/dist/src/funnel/consent.js +0 -58
  232. package/dist/src/funnel/credit-errors.d.ts +0 -31
  233. package/dist/src/funnel/credit-errors.js +0 -88
  234. package/dist/src/funnel/credit-notifier.d.ts +0 -44
  235. package/dist/src/funnel/credit-notifier.js +0 -74
  236. package/dist/src/funnel/disclosure.d.ts +0 -47
  237. package/dist/src/funnel/disclosure.js +0 -109
  238. package/dist/src/funnel/enrollment.d.ts +0 -36
  239. package/dist/src/funnel/enrollment.js +0 -64
  240. package/dist/src/funnel/environment.d.ts +0 -17
  241. package/dist/src/funnel/environment.js +0 -39
  242. package/dist/src/funnel/event-transport.d.ts +0 -51
  243. package/dist/src/funnel/event-transport.js +0 -199
  244. package/dist/src/funnel/events.d.ts +0 -42
  245. package/dist/src/funnel/events.js +0 -150
  246. package/dist/src/funnel/index.d.ts +0 -21
  247. package/dist/src/funnel/index.js +0 -21
  248. package/dist/src/funnel/insights.d.ts +0 -49
  249. package/dist/src/funnel/insights.js +0 -120
  250. package/dist/src/funnel/local-signals.d.ts +0 -15
  251. package/dist/src/funnel/local-signals.js +0 -68
  252. package/dist/src/funnel/message-transport.d.ts +0 -51
  253. package/dist/src/funnel/message-transport.js +0 -149
  254. package/dist/src/funnel/messages.d.ts +0 -55
  255. package/dist/src/funnel/messages.js +0 -160
  256. package/dist/src/funnel/payout.d.ts +0 -40
  257. package/dist/src/funnel/payout.js +0 -60
  258. package/dist/src/funnel/power-saver-notifier.d.ts +0 -44
  259. package/dist/src/funnel/power-saver-notifier.js +0 -92
  260. package/dist/src/funnel/precedence.d.ts +0 -16
  261. package/dist/src/funnel/precedence.js +0 -85
  262. package/dist/src/funnel/promo.d.ts +0 -41
  263. package/dist/src/funnel/promo.js +0 -144
  264. package/dist/src/funnel/rate-limit-notifier.d.ts +0 -55
  265. package/dist/src/funnel/rate-limit-notifier.js +0 -102
  266. package/dist/src/funnel/rotation.d.ts +0 -19
  267. package/dist/src/funnel/rotation.js +0 -70
  268. package/dist/src/funnel/state.d.ts +0 -13
  269. package/dist/src/funnel/state.js +0 -52
  270. package/dist/src/funnel/toggle-cooldown.d.ts +0 -17
  271. package/dist/src/funnel/toggle-cooldown.js +0 -32
  272. package/dist/src/funnel/types.d.ts +0 -98
  273. package/dist/src/funnel/types.js +0 -26
  274. package/dist/src/index.d.ts +0 -81
  275. package/dist/src/index.js +0 -609
  276. package/dist/src/infrastructure/in-memory-repositories.d.ts +0 -68
  277. package/dist/src/infrastructure/in-memory-repositories.js +0 -264
  278. package/dist/src/init/claudemd-generator.d.ts +0 -16
  279. package/dist/src/init/claudemd-generator.js +0 -368
  280. package/dist/src/init/executor.d.ts +0 -41
  281. package/dist/src/init/executor.js +0 -2142
  282. package/dist/src/init/helper-refresh.d.ts +0 -79
  283. package/dist/src/init/helper-refresh.js +0 -347
  284. package/dist/src/init/helper-signing.d.ts +0 -37
  285. package/dist/src/init/helper-signing.js +0 -67
  286. package/dist/src/init/helpers-generator.d.ts +0 -87
  287. package/dist/src/init/helpers-generator.js +0 -1456
  288. package/dist/src/init/index.d.ts +0 -13
  289. package/dist/src/init/index.js +0 -15
  290. package/dist/src/init/mcp-generator.d.ts +0 -26
  291. package/dist/src/init/mcp-generator.js +0 -126
  292. package/dist/src/init/memory-package-resolver.d.ts +0 -53
  293. package/dist/src/init/memory-package-resolver.js +0 -118
  294. package/dist/src/init/settings-generator.d.ts +0 -14
  295. package/dist/src/init/settings-generator.js +0 -462
  296. package/dist/src/init/statusline-generator.d.ts +0 -28
  297. package/dist/src/init/statusline-generator.js +0 -177
  298. package/dist/src/init/types.d.ts +0 -331
  299. package/dist/src/init/types.js +0 -276
  300. package/dist/src/log-filters.d.ts +0 -46
  301. package/dist/src/log-filters.js +0 -107
  302. package/dist/src/mcp-client.d.ts +0 -92
  303. package/dist/src/mcp-client.js +0 -397
  304. package/dist/src/mcp-server.d.ts +0 -163
  305. package/dist/src/mcp-server.js +0 -751
  306. package/dist/src/mcp-tools/agent-execute-core.d.ts +0 -115
  307. package/dist/src/mcp-tools/agent-execute-core.js +0 -587
  308. package/dist/src/mcp-tools/agent-tools.d.ts +0 -9
  309. package/dist/src/mcp-tools/agent-tools.js +0 -837
  310. package/dist/src/mcp-tools/agentbbs-tools.d.ts +0 -28
  311. package/dist/src/mcp-tools/agentbbs-tools.js +0 -394
  312. package/dist/src/mcp-tools/agentdb-tools.d.ts +0 -35
  313. package/dist/src/mcp-tools/agentdb-tools.js +0 -1473
  314. package/dist/src/mcp-tools/agenticow-loader.d.ts +0 -59
  315. package/dist/src/mcp-tools/agenticow-loader.js +0 -105
  316. package/dist/src/mcp-tools/agenticow-speculate-tools.d.ts +0 -24
  317. package/dist/src/mcp-tools/agenticow-speculate-tools.js +0 -209
  318. package/dist/src/mcp-tools/agenticow-tools.d.ts +0 -36
  319. package/dist/src/mcp-tools/agenticow-tools.js +0 -360
  320. package/dist/src/mcp-tools/analyze-tools.d.ts +0 -38
  321. package/dist/src/mcp-tools/analyze-tools.js +0 -346
  322. package/dist/src/mcp-tools/auto-install.d.ts +0 -83
  323. package/dist/src/mcp-tools/auto-install.js +0 -131
  324. package/dist/src/mcp-tools/autopilot-tools.d.ts +0 -12
  325. package/dist/src/mcp-tools/autopilot-tools.js +0 -231
  326. package/dist/src/mcp-tools/browser-intent-tools.d.ts +0 -162
  327. package/dist/src/mcp-tools/browser-intent-tools.js +0 -548
  328. package/dist/src/mcp-tools/browser-session-tools.d.ts +0 -27
  329. package/dist/src/mcp-tools/browser-session-tools.js +0 -398
  330. package/dist/src/mcp-tools/browser-tools.d.ts +0 -21
  331. package/dist/src/mcp-tools/browser-tools.js +0 -760
  332. package/dist/src/mcp-tools/business-pod-tools.d.ts +0 -20
  333. package/dist/src/mcp-tools/business-pod-tools.js +0 -169
  334. package/dist/src/mcp-tools/claims-tools.d.ts +0 -12
  335. package/dist/src/mcp-tools/claims-tools.js +0 -863
  336. package/dist/src/mcp-tools/config-tools.d.ts +0 -8
  337. package/dist/src/mcp-tools/config-tools.js +0 -411
  338. package/dist/src/mcp-tools/coordination-tools.d.ts +0 -13
  339. package/dist/src/mcp-tools/coordination-tools.js +0 -729
  340. package/dist/src/mcp-tools/daa-tools.d.ts +0 -13
  341. package/dist/src/mcp-tools/daa-tools.js +0 -534
  342. package/dist/src/mcp-tools/embeddings-tools.d.ts +0 -9
  343. package/dist/src/mcp-tools/embeddings-tools.js +0 -904
  344. package/dist/src/mcp-tools/github-tools.d.ts +0 -9
  345. package/dist/src/mcp-tools/github-tools.js +0 -659
  346. package/dist/src/mcp-tools/guidance-tools.d.ts +0 -15
  347. package/dist/src/mcp-tools/guidance-tools.js +0 -639
  348. package/dist/src/mcp-tools/hive-mind-tools.d.ts +0 -8
  349. package/dist/src/mcp-tools/hive-mind-tools.js +0 -953
  350. package/dist/src/mcp-tools/hooks-tools.d.ts +0 -65
  351. package/dist/src/mcp-tools/hooks-tools.js +0 -4824
  352. package/dist/src/mcp-tools/http-fetch-tools.d.ts +0 -55
  353. package/dist/src/mcp-tools/http-fetch-tools.js +0 -329
  354. package/dist/src/mcp-tools/index.d.ts +0 -34
  355. package/dist/src/mcp-tools/index.js +0 -40
  356. package/dist/src/mcp-tools/managed-agent-tools.d.ts +0 -22
  357. package/dist/src/mcp-tools/managed-agent-tools.js +0 -357
  358. package/dist/src/mcp-tools/memory-tools.d.ts +0 -14
  359. package/dist/src/mcp-tools/memory-tools.js +0 -1330
  360. package/dist/src/mcp-tools/metaharness-tools.d.ts +0 -51
  361. package/dist/src/mcp-tools/metaharness-tools.js +0 -684
  362. package/dist/src/mcp-tools/neural-tools.d.ts +0 -54
  363. package/dist/src/mcp-tools/neural-tools.js +0 -1168
  364. package/dist/src/mcp-tools/performance-tools.d.ts +0 -16
  365. package/dist/src/mcp-tools/performance-tools.js +0 -675
  366. package/dist/src/mcp-tools/progress-tools.d.ts +0 -14
  367. package/dist/src/mcp-tools/progress-tools.js +0 -348
  368. package/dist/src/mcp-tools/request-tracker.d.ts +0 -17
  369. package/dist/src/mcp-tools/request-tracker.js +0 -27
  370. package/dist/src/mcp-tools/ruvllm-tools.d.ts +0 -9
  371. package/dist/src/mcp-tools/ruvllm-tools.js +0 -355
  372. package/dist/src/mcp-tools/security-tools.d.ts +0 -18
  373. package/dist/src/mcp-tools/security-tools.js +0 -556
  374. package/dist/src/mcp-tools/session-tools.d.ts +0 -8
  375. package/dist/src/mcp-tools/session-tools.js +0 -517
  376. package/dist/src/mcp-tools/swarm-tools.d.ts +0 -37
  377. package/dist/src/mcp-tools/swarm-tools.js +0 -390
  378. package/dist/src/mcp-tools/system-tools.d.ts +0 -13
  379. package/dist/src/mcp-tools/system-tools.js +0 -688
  380. package/dist/src/mcp-tools/task-tools.d.ts +0 -8
  381. package/dist/src/mcp-tools/task-tools.js +0 -487
  382. package/dist/src/mcp-tools/terminal-tools.d.ts +0 -8
  383. package/dist/src/mcp-tools/terminal-tools.js +0 -306
  384. package/dist/src/mcp-tools/testgen-tools.d.ts +0 -26
  385. package/dist/src/mcp-tools/testgen-tools.js +0 -168
  386. package/dist/src/mcp-tools/tool-loop-guardrail.d.ts +0 -31
  387. package/dist/src/mcp-tools/tool-loop-guardrail.js +0 -71
  388. package/dist/src/mcp-tools/transfer-tools.d.ts +0 -14
  389. package/dist/src/mcp-tools/transfer-tools.js +0 -447
  390. package/dist/src/mcp-tools/types.d.ts +0 -8
  391. package/dist/src/mcp-tools/types.js +0 -8
  392. package/dist/src/mcp-tools/validate-input.d.ts +0 -9
  393. package/dist/src/mcp-tools/validate-input.js +0 -9
  394. package/dist/src/mcp-tools/wasm-agent-tools.d.ts +0 -13
  395. package/dist/src/mcp-tools/wasm-agent-tools.js +0 -840
  396. package/dist/src/mcp-tools/workflow-tools.d.ts +0 -8
  397. package/dist/src/mcp-tools/workflow-tools.js +0 -884
  398. package/dist/src/memory/bge-embedder.d.ts +0 -25
  399. package/dist/src/memory/bge-embedder.js +0 -121
  400. package/dist/src/memory/cross-encoder-rerank.d.ts +0 -33
  401. package/dist/src/memory/cross-encoder-rerank.js +0 -123
  402. package/dist/src/memory/embedding-policy.d.ts +0 -21
  403. package/dist/src/memory/embedding-policy.js +0 -30
  404. package/dist/src/memory/embedding-quantization.d.ts +0 -62
  405. package/dist/src/memory/embedding-quantization.js +0 -156
  406. package/dist/src/memory/ewc-consolidation.d.ts +0 -305
  407. package/dist/src/memory/ewc-consolidation.js +0 -611
  408. package/dist/src/memory/graph-edge-writer.d.ts +0 -95
  409. package/dist/src/memory/graph-edge-writer.js +0 -217
  410. package/dist/src/memory/hybrid-retrieval.d.ts +0 -77
  411. package/dist/src/memory/hybrid-retrieval.js +0 -192
  412. package/dist/src/memory/intelligence.d.ts +0 -405
  413. package/dist/src/memory/intelligence.js +0 -1316
  414. package/dist/src/memory/lucene-bm25.d.ts +0 -19
  415. package/dist/src/memory/lucene-bm25.js +0 -308
  416. package/dist/src/memory/memory-bridge.d.ts +0 -537
  417. package/dist/src/memory/memory-bridge.js +0 -2460
  418. package/dist/src/memory/memory-initializer.d.ts +0 -556
  419. package/dist/src/memory/memory-initializer.js +0 -3021
  420. package/dist/src/memory/neural-package-bridge.d.ts +0 -48
  421. package/dist/src/memory/neural-package-bridge.js +0 -87
  422. package/dist/src/memory/rabitq-index.d.ts +0 -60
  423. package/dist/src/memory/rabitq-index.js +0 -242
  424. package/dist/src/memory/sona-optimizer.d.ts +0 -267
  425. package/dist/src/memory/sona-optimizer.js +0 -779
  426. package/dist/src/memory/structured-distill.d.ts +0 -48
  427. package/dist/src/memory/structured-distill.js +0 -125
  428. package/dist/src/output.d.ts +0 -9
  429. package/dist/src/output.js +0 -9
  430. package/dist/src/parser.d.ts +0 -89
  431. package/dist/src/parser.js +0 -516
  432. package/dist/src/plugins/manager.d.ts +0 -133
  433. package/dist/src/plugins/manager.js +0 -415
  434. package/dist/src/plugins/store/discovery.d.ts +0 -99
  435. package/dist/src/plugins/store/discovery.js +0 -1224
  436. package/dist/src/plugins/store/index.d.ts +0 -76
  437. package/dist/src/plugins/store/index.js +0 -141
  438. package/dist/src/plugins/store/search.d.ts +0 -46
  439. package/dist/src/plugins/store/search.js +0 -230
  440. package/dist/src/plugins/store/types.d.ts +0 -279
  441. package/dist/src/plugins/store/types.js +0 -7
  442. package/dist/src/plugins/tests/demo-plugin-store.d.ts +0 -7
  443. package/dist/src/plugins/tests/demo-plugin-store.js +0 -126
  444. package/dist/src/plugins/tests/standalone-test.d.ts +0 -12
  445. package/dist/src/plugins/tests/standalone-test.js +0 -188
  446. package/dist/src/plugins/tests/test-plugin-store.d.ts +0 -7
  447. package/dist/src/plugins/tests/test-plugin-store.js +0 -206
  448. package/dist/src/production/circuit-breaker.d.ts +0 -101
  449. package/dist/src/production/circuit-breaker.js +0 -241
  450. package/dist/src/production/error-handler.d.ts +0 -92
  451. package/dist/src/production/error-handler.js +0 -299
  452. package/dist/src/production/index.d.ts +0 -23
  453. package/dist/src/production/index.js +0 -18
  454. package/dist/src/production/monitoring.d.ts +0 -161
  455. package/dist/src/production/monitoring.js +0 -356
  456. package/dist/src/production/rate-limiter.d.ts +0 -80
  457. package/dist/src/production/rate-limiter.js +0 -201
  458. package/dist/src/production/retry.d.ts +0 -48
  459. package/dist/src/production/retry.js +0 -179
  460. package/dist/src/prompt.d.ts +0 -44
  461. package/dist/src/prompt.js +0 -501
  462. package/dist/src/runtime/headless.d.ts +0 -60
  463. package/dist/src/runtime/headless.js +0 -284
  464. package/dist/src/runtime/parent-death-watchdog.d.ts +0 -42
  465. package/dist/src/runtime/parent-death-watchdog.js +0 -70
  466. package/dist/src/ruvector/agent-wasm.d.ts +0 -228
  467. package/dist/src/ruvector/agent-wasm.js +0 -463
  468. package/dist/src/ruvector/ast-analyzer.d.ts +0 -67
  469. package/dist/src/ruvector/ast-analyzer.js +0 -277
  470. package/dist/src/ruvector/codemods/engine.d.ts +0 -45
  471. package/dist/src/ruvector/codemods/engine.js +0 -291
  472. package/dist/src/ruvector/codemods/scope-analysis.d.ts +0 -29
  473. package/dist/src/ruvector/codemods/scope-analysis.js +0 -162
  474. package/dist/src/ruvector/coverage-router.d.ts +0 -160
  475. package/dist/src/ruvector/coverage-router.js +0 -531
  476. package/dist/src/ruvector/coverage-tools.d.ts +0 -33
  477. package/dist/src/ruvector/coverage-tools.js +0 -157
  478. package/dist/src/ruvector/diff-classifier.d.ts +0 -175
  479. package/dist/src/ruvector/diff-classifier.js +0 -699
  480. package/dist/src/ruvector/diskann-backend.d.ts +0 -78
  481. package/dist/src/ruvector/diskann-backend.js +0 -310
  482. package/dist/src/ruvector/enhanced-model-router.d.ts +0 -172
  483. package/dist/src/ruvector/enhanced-model-router.js +0 -577
  484. package/dist/src/ruvector/graph-analyzer.d.ts +0 -187
  485. package/dist/src/ruvector/graph-analyzer.js +0 -929
  486. package/dist/src/ruvector/graph-backend.d.ts +0 -79
  487. package/dist/src/ruvector/graph-backend.js +0 -220
  488. package/dist/src/ruvector/index.d.ts +0 -38
  489. package/dist/src/ruvector/index.js +0 -86
  490. package/dist/src/ruvector/lora-adapter.d.ts +0 -292
  491. package/dist/src/ruvector/lora-adapter.js +0 -710
  492. package/dist/src/ruvector/model-prices.d.ts +0 -50
  493. package/dist/src/ruvector/model-prices.js +0 -72
  494. package/dist/src/ruvector/model-router.d.ts +0 -408
  495. package/dist/src/ruvector/model-router.js +0 -1162
  496. package/dist/src/ruvector/neural-router.d.ts +0 -182
  497. package/dist/src/ruvector/neural-router.js +0 -825
  498. package/dist/src/ruvector/output-verifier.d.ts +0 -83
  499. package/dist/src/ruvector/output-verifier.js +0 -277
  500. package/dist/src/ruvector/q-learning-router.d.ts +0 -227
  501. package/dist/src/ruvector/q-learning-router.js +0 -721
  502. package/dist/src/ruvector/router-calibrator.d.ts +0 -65
  503. package/dist/src/ruvector/router-calibrator.js +0 -120
  504. package/dist/src/ruvector/router-parallel-recorder.d.ts +0 -127
  505. package/dist/src/ruvector/router-parallel-recorder.js +0 -183
  506. package/dist/src/ruvector/router-trajectory.d.ts +0 -194
  507. package/dist/src/ruvector/router-trajectory.js +0 -281
  508. package/dist/src/ruvector/run-transcript-recorder.d.ts +0 -154
  509. package/dist/src/ruvector/run-transcript-recorder.js +0 -209
  510. package/dist/src/ruvector/ruvllm-wasm.d.ts +0 -179
  511. package/dist/src/ruvector/ruvllm-wasm.js +0 -379
  512. package/dist/src/ruvector/semantic-router.d.ts +0 -77
  513. package/dist/src/ruvector/semantic-router.js +0 -178
  514. package/dist/src/ruvector/task-embedder.d.ts +0 -56
  515. package/dist/src/ruvector/task-embedder.js +0 -237
  516. package/dist/src/ruvector/trajectory-tree.d.ts +0 -113
  517. package/dist/src/ruvector/trajectory-tree.js +0 -237
  518. package/dist/src/ruvector/vector-db.d.ts +0 -73
  519. package/dist/src/ruvector/vector-db.js +0 -301
  520. package/dist/src/ruvector/wasm-embedder.d.ts +0 -13
  521. package/dist/src/ruvector/wasm-embedder.js +0 -143
  522. package/dist/src/services/agentic-flow-bridge.d.ts +0 -50
  523. package/dist/src/services/agentic-flow-bridge.js +0 -95
  524. package/dist/src/services/ai-job-dedup.d.ts +0 -61
  525. package/dist/src/services/ai-job-dedup.js +0 -136
  526. package/dist/src/services/checkpoint-gate.d.ts +0 -140
  527. package/dist/src/services/checkpoint-gate.js +0 -223
  528. package/dist/src/services/claim-service.d.ts +0 -204
  529. package/dist/src/services/claim-service.js +0 -818
  530. package/dist/src/services/config-file-manager.d.ts +0 -37
  531. package/dist/src/services/config-file-manager.js +0 -224
  532. package/dist/src/services/container-worker-pool.d.ts +0 -204
  533. package/dist/src/services/container-worker-pool.js +0 -589
  534. package/dist/src/services/daemon-autostart.d.ts +0 -17
  535. package/dist/src/services/daemon-autostart.js +0 -102
  536. package/dist/src/services/distill-oracle.d.ts +0 -190
  537. package/dist/src/services/distill-oracle.js +0 -349
  538. package/dist/src/services/distill-tuning.d.ts +0 -111
  539. package/dist/src/services/distill-tuning.js +0 -510
  540. package/dist/src/services/evolve-proof.d.ts +0 -253
  541. package/dist/src/services/evolve-proof.js +0 -343
  542. package/dist/src/services/fable-harness.d.ts +0 -207
  543. package/dist/src/services/fable-harness.js +0 -388
  544. package/dist/src/services/git-workspace-identity.d.ts +0 -42
  545. package/dist/src/services/git-workspace-identity.js +0 -99
  546. package/dist/src/services/global-ai-budget.d.ts +0 -135
  547. package/dist/src/services/global-ai-budget.js +0 -415
  548. package/dist/src/services/harness-benchmark.d.ts +0 -68
  549. package/dist/src/services/harness-benchmark.js +0 -94
  550. package/dist/src/services/harness-canary.d.ts +0 -60
  551. package/dist/src/services/harness-canary.js +0 -69
  552. package/dist/src/services/harness-corpus-harvester.d.ts +0 -51
  553. package/dist/src/services/harness-corpus-harvester.js +0 -74
  554. package/dist/src/services/harness-flywheel-generations.d.ts +0 -115
  555. package/dist/src/services/harness-flywheel-generations.js +0 -360
  556. package/dist/src/services/harness-flywheel-runtime.d.ts +0 -25
  557. package/dist/src/services/harness-flywheel-runtime.js +0 -92
  558. package/dist/src/services/harness-flywheel.d.ts +0 -48
  559. package/dist/src/services/harness-flywheel.js +0 -207
  560. package/dist/src/services/harness-frozen-eval.d.ts +0 -22
  561. package/dist/src/services/harness-frozen-eval.js +0 -66
  562. package/dist/src/services/harness-hosts.d.ts +0 -40
  563. package/dist/src/services/harness-hosts.js +0 -88
  564. package/dist/src/services/harness-improvement-ledger.d.ts +0 -63
  565. package/dist/src/services/harness-improvement-ledger.js +0 -101
  566. package/dist/src/services/harness-loop.d.ts +0 -53
  567. package/dist/src/services/harness-loop.js +0 -85
  568. package/dist/src/services/harness-qualification.d.ts +0 -66
  569. package/dist/src/services/harness-qualification.js +0 -143
  570. package/dist/src/services/harness-replay.d.ts +0 -37
  571. package/dist/src/services/harness-replay.js +0 -92
  572. package/dist/src/services/harness-verify.d.ts +0 -33
  573. package/dist/src/services/harness-verify.js +0 -26
  574. package/dist/src/services/harness-worker.d.ts +0 -23
  575. package/dist/src/services/harness-worker.js +0 -66
  576. package/dist/src/services/headless-worker-executor.d.ts +0 -358
  577. package/dist/src/services/headless-worker-executor.js +0 -1269
  578. package/dist/src/services/index.d.ts +0 -13
  579. package/dist/src/services/index.js +0 -11
  580. package/dist/src/services/memory-backup.d.ts +0 -53
  581. package/dist/src/services/memory-backup.js +0 -198
  582. package/dist/src/services/memory-distillation.d.ts +0 -41
  583. package/dist/src/services/memory-distillation.js +0 -277
  584. package/dist/src/services/native-training.d.ts +0 -68
  585. package/dist/src/services/native-training.js +0 -141
  586. package/dist/src/services/registry-api.d.ts +0 -58
  587. package/dist/src/services/registry-api.js +0 -146
  588. package/dist/src/services/repo-supervisor.d.ts +0 -70
  589. package/dist/src/services/repo-supervisor.js +0 -228
  590. package/dist/src/services/ruvector-training.d.ts +0 -222
  591. package/dist/src/services/ruvector-training.js +0 -693
  592. package/dist/src/services/swarm-memory-branches.d.ts +0 -135
  593. package/dist/src/services/swarm-memory-branches.js +0 -213
  594. package/dist/src/services/weight-eft.d.ts +0 -305
  595. package/dist/src/services/weight-eft.js +0 -296
  596. package/dist/src/services/worker-daemon.d.ts +0 -439
  597. package/dist/src/services/worker-daemon.js +0 -1865
  598. package/dist/src/services/worker-queue.d.ts +0 -194
  599. package/dist/src/services/worker-queue.js +0 -513
  600. package/dist/src/services/workspace-lease.d.ts +0 -55
  601. package/dist/src/services/workspace-lease.js +0 -191
  602. package/dist/src/suggest.d.ts +0 -53
  603. package/dist/src/suggest.js +0 -200
  604. package/dist/src/transfer/anonymization/index.d.ts +0 -25
  605. package/dist/src/transfer/anonymization/index.js +0 -175
  606. package/dist/src/transfer/deploy-seraphine.d.ts +0 -13
  607. package/dist/src/transfer/deploy-seraphine.js +0 -205
  608. package/dist/src/transfer/export.d.ts +0 -25
  609. package/dist/src/transfer/export.js +0 -113
  610. package/dist/src/transfer/index.d.ts +0 -12
  611. package/dist/src/transfer/index.js +0 -31
  612. package/dist/src/transfer/ipfs/client.d.ts +0 -109
  613. package/dist/src/transfer/ipfs/client.js +0 -307
  614. package/dist/src/transfer/ipfs/upload.d.ts +0 -95
  615. package/dist/src/transfer/ipfs/upload.js +0 -413
  616. package/dist/src/transfer/models/seraphine.d.ts +0 -72
  617. package/dist/src/transfer/models/seraphine.js +0 -373
  618. package/dist/src/transfer/serialization/cfp.d.ts +0 -49
  619. package/dist/src/transfer/serialization/cfp.js +0 -183
  620. package/dist/src/transfer/storage/gcs.d.ts +0 -82
  621. package/dist/src/transfer/storage/gcs.js +0 -272
  622. package/dist/src/transfer/storage/index.d.ts +0 -6
  623. package/dist/src/transfer/storage/index.js +0 -6
  624. package/dist/src/transfer/store/discovery.d.ts +0 -84
  625. package/dist/src/transfer/store/discovery.js +0 -382
  626. package/dist/src/transfer/store/download.d.ts +0 -70
  627. package/dist/src/transfer/store/download.js +0 -334
  628. package/dist/src/transfer/store/index.d.ts +0 -84
  629. package/dist/src/transfer/store/index.js +0 -153
  630. package/dist/src/transfer/store/publish.d.ts +0 -76
  631. package/dist/src/transfer/store/publish.js +0 -294
  632. package/dist/src/transfer/store/registry.d.ts +0 -58
  633. package/dist/src/transfer/store/registry.js +0 -285
  634. package/dist/src/transfer/store/search.d.ts +0 -54
  635. package/dist/src/transfer/store/search.js +0 -232
  636. package/dist/src/transfer/store/tests/standalone-test.d.ts +0 -12
  637. package/dist/src/transfer/store/tests/standalone-test.js +0 -190
  638. package/dist/src/transfer/store/types.d.ts +0 -193
  639. package/dist/src/transfer/store/types.js +0 -6
  640. package/dist/src/transfer/test-seraphine.d.ts +0 -6
  641. package/dist/src/transfer/test-seraphine.js +0 -105
  642. package/dist/src/transfer/tests/test-store.d.ts +0 -7
  643. package/dist/src/transfer/tests/test-store.js +0 -214
  644. package/dist/src/transfer/types.d.ts +0 -245
  645. package/dist/src/transfer/types.js +0 -6
  646. package/dist/src/types.d.ts +0 -13
  647. package/dist/src/types.js +0 -13
  648. package/dist/src/update/checker.d.ts +0 -34
  649. package/dist/src/update/checker.js +0 -191
  650. package/dist/src/update/executor.d.ts +0 -33
  651. package/dist/src/update/executor.js +0 -217
  652. package/dist/src/update/index.d.ts +0 -33
  653. package/dist/src/update/index.js +0 -64
  654. package/dist/src/update/rate-limiter.d.ts +0 -20
  655. package/dist/src/update/rate-limiter.js +0 -96
  656. package/dist/src/update/validator.d.ts +0 -17
  657. package/dist/src/update/validator.js +0 -123
@@ -1,4331 +0,0 @@
1
- /**
2
- * V3 CLI Neural Command
3
- * Neural pattern training, MoE, Flash Attention, pattern learning
4
- *
5
- * Created with ❤️ by ruv.io
6
- */
7
- import { output } from '../output.js';
8
- // Train subcommand - REAL WASM training with RuVector
9
- const trainCommand = {
10
- name: 'train',
11
- description: 'Train neural patterns with WASM SIMD acceleration (MicroLoRA + Flash Attention)',
12
- options: [
13
- { name: 'pattern', short: 'p', type: 'string', description: 'Pattern type: coordination, optimization, prediction, security, testing', default: 'coordination' },
14
- { name: 'epochs', short: 'e', type: 'number', description: 'Number of training epochs', default: '50' },
15
- { name: 'data', short: 'd', type: 'string', description: 'Training data file or inline JSON' },
16
- { name: 'model', short: 'm', type: 'string', description: 'Model ID to train' },
17
- { name: 'learning-rate', short: 'l', type: 'number', description: 'Learning rate', default: '0.01' },
18
- { name: 'batch-size', short: 'b', type: 'number', description: 'Batch size', default: '32' },
19
- { name: 'dim', type: 'number', description: 'Embedding dimension (max 256)', default: '256' },
20
- { name: 'wasm', short: 'w', type: 'boolean', description: 'Use RuVector WASM acceleration', default: 'true' },
21
- { name: 'flash', type: 'boolean', description: 'Enable Flash Attention (2.49x-7.47x speedup)', default: 'true' },
22
- { name: 'moe', type: 'boolean', description: 'Enable Mixture of Experts routing', default: 'false' },
23
- { name: 'hyperbolic', type: 'boolean', description: 'Enable hyperbolic attention for hierarchical patterns', default: 'false' },
24
- { name: 'contrastive', type: 'boolean', description: 'Use contrastive learning (InfoNCE)', default: 'true' },
25
- { name: 'curriculum', type: 'boolean', description: 'Enable curriculum learning', default: 'false' },
26
- { name: 'backend', type: 'string', description: 'Training backend: auto (native when available), native (@ruvector/ruvllm TrainingPipeline, disk checkpoints), wasm (RuVector MicroLoRA/InfoNCE)', default: 'auto' },
27
- { name: 'val-split', type: 'number', description: 'Validation holdout fraction 0..1 (native backend). >0 reports Best Val Loss + early stopping; 0 disables', default: '0.1' },
28
- { name: 'resume', type: 'string', description: 'Resume native training from a checkpoint path (weights on 2.5.7; epoch position on >=2.6.0). Native backend only', default: '' },
29
- ],
30
- examples: [
31
- { command: 'claude-flow neural train -p coordination -e 100', description: 'Train coordination patterns' },
32
- { command: 'claude-flow neural train -d ./training-data.json --flash', description: 'Train from file with Flash Attention' },
33
- { command: 'claude-flow neural train -p security --wasm --contrastive', description: 'Security patterns with contrastive learning' },
34
- ],
35
- action: async (ctx) => {
36
- const patternType = (ctx.flags.pattern || ctx.flags.patternType || ctx.flags['pattern-type']) || 'coordination';
37
- const epochs = parseInt(ctx.flags.epochs || '50', 10);
38
- const learningRate = parseFloat(ctx.flags['learning-rate'] || '0.01');
39
- const batchSize = parseInt(ctx.flags['batch-size'] || '32', 10);
40
- const dim = Math.min(parseInt(ctx.flags.dim || '256', 10), 256);
41
- // #2549 follow-up — backend routing: 'native' = @ruvector/ruvllm
42
- // TrainingPipeline (real epochs/early-stopping/disk checkpoints),
43
- // 'wasm' = RuVector MicroLoRA/InfoNCE (pre-3.19 behavior),
44
- // 'auto' = native when the module resolves, else wasm.
45
- const backendFlag = String(ctx.flags.backend || 'auto');
46
- // Feature: validation split + resume (native TrainingPipeline leg).
47
- const valSplitRaw = parseFloat(ctx.flags['val-split'] ?? '0.1');
48
- const valSplit = Number.isFinite(valSplitRaw) ? Math.max(0, Math.min(1, valSplitRaw)) : 0.1;
49
- const resumePath = ctx.flags.resume ? String(ctx.flags.resume) : undefined;
50
- // --resume is a native-only capability; refuse the WASM combination up
51
- // front so the user gets a clear error rather than a silently-ignored flag.
52
- if (resumePath && backendFlag === 'wasm') {
53
- output.writeln();
54
- output.writeln(output.error('--resume is only supported by the native backend; drop --backend wasm.'));
55
- return { success: false, exitCode: 1 };
56
- }
57
- const useWasm = ctx.flags.wasm !== false;
58
- const useFlash = ctx.flags.flash !== false;
59
- const useMoE = ctx.flags.moe === true;
60
- const useHyperbolic = ctx.flags.hyperbolic === true;
61
- const useContrastive = ctx.flags.contrastive !== false;
62
- const useCurriculum = ctx.flags.curriculum === true;
63
- const dataFile = ctx.flags.data;
64
- output.writeln();
65
- output.writeln(output.bold('Neural Pattern Training (RuVector WASM)'));
66
- output.writeln(output.dim('─'.repeat(55)));
67
- const spinner = output.createSpinner({ text: 'Initializing RuVector training systems...', spinner: 'dots' });
68
- spinner.start();
69
- try {
70
- // Import RuVector training service
71
- const ruvector = await import('../services/ruvector-training.js');
72
- const { generateEmbedding } = await import('../memory/memory-initializer.js');
73
- const { initializeIntelligence, recordStep, recordTrajectory, getIntelligenceStats, flushPatterns, getPersistenceStatus } = await import('../memory/intelligence.js');
74
- // Initialize RuVector WASM training
75
- let wasmFeatures = [];
76
- if (useWasm) {
77
- const initResult = await ruvector.initializeTraining({
78
- dim,
79
- learningRate,
80
- alpha: 0.1,
81
- trajectoryCapacity: epochs * batchSize,
82
- useFlashAttention: useFlash,
83
- useMoE,
84
- useHyperbolic,
85
- totalSteps: useCurriculum ? epochs : undefined,
86
- warmupSteps: useCurriculum ? Math.floor(epochs * 0.1) : undefined,
87
- });
88
- if (initResult.success) {
89
- wasmFeatures = initResult.features;
90
- const backendLabel = initResult.backend === 'wasm' ? 'WASM' : 'JS fallback';
91
- spinner.setText(`RuVector initialized [${backendLabel}]: ${wasmFeatures.join(', ')}`);
92
- }
93
- else {
94
- output.writeln(output.warning(`WASM init failed: ${initResult.error} - falling back`));
95
- }
96
- }
97
- // Also initialize SONA + ReasoningBank for persistence
98
- await initializeIntelligence({
99
- loraLearningRate: learningRate,
100
- maxTrajectorySize: epochs
101
- });
102
- // Pattern type to operator mapping
103
- const operatorMap = {
104
- coordination: ruvector.OperatorType.COORDINATION,
105
- optimization: ruvector.OperatorType.OPTIMIZATION,
106
- prediction: ruvector.OperatorType.ROUTING,
107
- security: ruvector.OperatorType.SECURITY,
108
- testing: ruvector.OperatorType.TESTING,
109
- debugging: ruvector.OperatorType.DEBUGGING,
110
- memory: ruvector.OperatorType.MEMORY,
111
- reasoning: ruvector.OperatorType.REASONING,
112
- };
113
- const operatorType = operatorMap[patternType] ?? ruvector.OperatorType.GENERAL;
114
- spinner.setText(`Training ${patternType} patterns...`);
115
- // Training data - load from file or generate synthetic
116
- let trainingData = [];
117
- if (dataFile) {
118
- const fs = await import('fs');
119
- if (fs.existsSync(dataFile)) {
120
- const raw = fs.readFileSync(dataFile, 'utf8');
121
- trainingData = JSON.parse(raw);
122
- }
123
- else {
124
- spinner.fail(`Training data file not found: ${dataFile}`);
125
- return { success: false, exitCode: 1 };
126
- }
127
- }
128
- else {
129
- // Generate synthetic training data based on pattern type
130
- const templates = {
131
- coordination: [
132
- 'Route task to coder agent for implementation',
133
- 'Coordinate researcher and architect for design phase',
134
- 'Distribute workload across mesh topology',
135
- 'Synchronize agents via gossip protocol',
136
- 'Balance load between active workers',
137
- 'Spawn hierarchical swarm for complex task',
138
- 'Assign reviewer to completed implementation'
139
- ],
140
- optimization: [
141
- 'Apply Int8 quantization for memory reduction',
142
- 'Enable HNSW indexing for faster search',
143
- 'Batch operations for throughput improvement',
144
- 'Cache frequently accessed patterns',
145
- 'Prune unused neural pathways',
146
- 'Use Flash Attention for large sequences',
147
- 'Enable SIMD for vector operations'
148
- ],
149
- prediction: [
150
- 'Predict optimal agent for task type',
151
- 'Forecast resource requirements',
152
- 'Anticipate failure modes and mitigate',
153
- 'Estimate completion time for workflow',
154
- 'Predict pattern similarity before search'
155
- ],
156
- security: [
157
- 'Validate input at system boundaries',
158
- 'Check for path traversal attempts',
159
- 'Sanitize user-provided data',
160
- 'Apply parameterized queries for SQL',
161
- 'Verify JWT token signatures',
162
- 'Audit sensitive operation access'
163
- ],
164
- testing: [
165
- 'Generate unit tests for function',
166
- 'Create integration test suite',
167
- 'Mock external dependencies',
168
- 'Assert expected outcomes',
169
- 'Coverage gap analysis'
170
- ]
171
- };
172
- const patterns = templates[patternType] || templates.coordination;
173
- for (let i = 0; i < epochs; i++) {
174
- trainingData.push({
175
- content: patterns[i % patterns.length],
176
- type: patternType
177
- });
178
- }
179
- }
180
- // Training metrics
181
- const startTime = Date.now();
182
- const epochTimes = [];
183
- let patternsRecorded = 0;
184
- let trajectoriesCompleted = 0;
185
- let totalLoss = 0;
186
- let adaptations = 0;
187
- // Generate embeddings for training data
188
- const embeddings = [];
189
- spinner.setText('Generating embeddings...');
190
- for (const item of trainingData.slice(0, Math.min(100, trainingData.length))) {
191
- const embeddingResult = await generateEmbedding(item.content);
192
- if (embeddingResult && embeddingResult.embedding) {
193
- // Convert to Float32Array and resize to dim
194
- const embeddingArray = embeddingResult.embedding;
195
- const resized = new Float32Array(dim);
196
- for (let i = 0; i < Math.min(embeddingArray.length, dim); i++) {
197
- resized[i] = embeddingArray[i];
198
- }
199
- embeddings.push(resized);
200
- }
201
- }
202
- spinner.setText(`Training with ${embeddings.length} embeddings...`);
203
- // #2549 — native TrainingPipeline leg. In 'auto'/'native' mode the
204
- // LoRA training runs through @ruvector/ruvllm with the checkpoint
205
- // taken from the TRAINED pipeline (the old best-effort block saved
206
- // a fresh adapter's untrained weights). SONA/ReasoningBank
207
- // persistence in the loop below runs regardless of backend.
208
- const nativeTraining = await import('../services/native-training.js');
209
- const useNative = backendFlag === 'native'
210
- || (backendFlag === 'auto' && nativeTraining.nativeTrainingAvailable());
211
- // --resume only works on the native pipeline; if native is unavailable
212
- // (module absent), fail loudly rather than silently fresh-train.
213
- if (resumePath && !useNative) {
214
- spinner.fail('--resume requires the native @ruvector/ruvllm backend, which is not available');
215
- return { success: false, exitCode: 1 };
216
- }
217
- let nativeResult = null;
218
- if (useNative) {
219
- spinner.setText(`Training ${patternType} on native @ruvector/ruvllm pipeline...`);
220
- const path = await import('path');
221
- try {
222
- nativeResult = await nativeTraining.runNativeTraining({
223
- embeddings,
224
- epochs,
225
- batchSize,
226
- learningRate,
227
- dim,
228
- validationSplit: valSplit,
229
- resumeFrom: resumePath,
230
- checkpointPath: path.join(process.cwd(), '.claude-flow', 'neural', `lora-checkpoint-${Date.now()}.json`),
231
- });
232
- }
233
- catch (err) {
234
- // ResumeFailedError — an explicit --resume that could not load is a
235
- // loud, exit-1 failure, never a silent fall-through to fresh training.
236
- spinner.fail(`Resume failed: ${err.message}`);
237
- return { success: false, exitCode: 1 };
238
- }
239
- if (!nativeResult && backendFlag === 'native') {
240
- spinner.fail('Native backend requested (--backend native) but @ruvector/ruvllm training failed');
241
- return { success: false, exitCode: 1 };
242
- }
243
- }
244
- // Native handles the LoRA leg; WASM contrastive runs when native
245
- // didn't (absent module, or explicit --backend wasm).
246
- const runWasmLeg = !nativeResult;
247
- // Main training loop with WASM acceleration
248
- for (let epoch = 0; epoch < epochs; epoch++) {
249
- const epochStart = performance.now();
250
- // Get curriculum difficulty if enabled
251
- const difficulty = useCurriculum ? ruvector.getCurriculumDifficulty(epoch) : 1.0;
252
- // Process batch
253
- const batchStart = (epoch * batchSize) % embeddings.length;
254
- const batch = embeddings.slice(batchStart, batchStart + batchSize);
255
- if (batch.length === 0)
256
- continue;
257
- // Training step with contrastive learning
258
- if (runWasmLeg && useContrastive && batch.length >= 3 && useWasm && wasmFeatures.length > 0) {
259
- const anchor = batch[0];
260
- const positives = [batch[1]];
261
- const negatives = batch.slice(2);
262
- try {
263
- // Compute contrastive loss
264
- const { loss, gradient } = ruvector.computeContrastiveLoss(anchor, positives, negatives);
265
- totalLoss += loss;
266
- // Scale gradient by difficulty
267
- const scaledGradient = new Float32Array(gradient.length);
268
- for (let i = 0; i < gradient.length; i++) {
269
- scaledGradient[i] = gradient[i] * difficulty;
270
- }
271
- // Train with MicroLoRA
272
- await ruvector.trainPattern(anchor, scaledGradient, operatorType);
273
- adaptations++;
274
- // Record trajectory for learning
275
- const baselineMs = 10; // Baseline execution time
276
- const executionMs = performance.now() - epochStart;
277
- ruvector.recordTrajectory(anchor, operatorType, useFlash ? 1 : 0, executionMs, baselineMs);
278
- }
279
- catch {
280
- // WASM training failed, fall back to basic
281
- }
282
- }
283
- // Also record in SONA/ReasoningBank for persistence
284
- const item = trainingData[epoch % trainingData.length];
285
- await recordStep({
286
- type: 'action',
287
- content: item.content,
288
- metadata: { epoch, patternType, learningRate, difficulty }
289
- });
290
- patternsRecorded++;
291
- // Record trajectory every 10 epochs
292
- if ((epoch + 1) % 10 === 0 || epoch === epochs - 1) {
293
- const steps = trainingData.slice(Math.max(0, epoch - 9), epoch + 1).map(d => ({ type: 'action', content: d.content }));
294
- await recordTrajectory(steps, 'success');
295
- trajectoriesCompleted++;
296
- }
297
- const epochTime = performance.now() - epochStart;
298
- epochTimes.push(epochTime);
299
- // Update progress
300
- const progress = Math.round(((epoch + 1) / epochs) * 100);
301
- const avgEpochTime = epochTimes.reduce((a, b) => a + b, 0) / epochTimes.length;
302
- const eta = Math.round((epochs - epoch - 1) * avgEpochTime / 1000);
303
- spinner.setText(`Training ${patternType} patterns... ${progress}% (ETA: ${eta}s, loss: ${(totalLoss / Math.max(1, epoch + 1)).toFixed(4)})`);
304
- }
305
- const totalTime = Date.now() - startTime;
306
- // Get RuVector stats
307
- const ruvectorStats = useWasm && wasmFeatures.length > 0 ? ruvector.getTrainingStats() : null;
308
- const trajectoryStats = ruvectorStats?.trajectoryStats;
309
- // Benchmark if WASM was used
310
- let benchmark = null;
311
- if (useWasm && wasmFeatures.length > 0) {
312
- try {
313
- spinner.setText('Running benchmark...');
314
- benchmark = await ruvector.benchmarkTraining(dim, 100);
315
- }
316
- catch {
317
- // Benchmark failed, continue
318
- }
319
- }
320
- // Get SONA stats
321
- const stats = getIntelligenceStats();
322
- spinner.succeed(`Training complete: ${epochs} epochs in ${(totalTime / 1000).toFixed(1)}s`);
323
- // Flush patterns to disk
324
- flushPatterns();
325
- const persistence = getPersistenceStatus();
326
- // Checkpoint: when the native pipeline trained, its checkpoint (the
327
- // TRAINED weights) was already written by runNativeTraining. The
328
- // pre-3.19 fallback below saved a FRESH adapter's weights — only
329
- // meaningful as a fallback when the native leg didn't run.
330
- if (!nativeResult?.checkpointPath) {
331
- try {
332
- const { LoRAAdapter } = await import('../ruvector/lora-adapter.js');
333
- const path = await import('path');
334
- const cpDir = path.join(process.cwd(), '.claude-flow', 'neural');
335
- const cpPath = path.join(cpDir, `lora-checkpoint-${Date.now()}.json`);
336
- const adapter = new LoRAAdapter({ inputDim: dim, outputDim: dim, rank: 4 });
337
- await adapter.initBackend();
338
- await adapter.saveCheckpoint(cpPath);
339
- }
340
- catch { /* checkpoint save is best-effort */ }
341
- }
342
- output.writeln();
343
- // Display results
344
- const tableData = [
345
- { metric: 'Pattern Type', value: patternType },
346
- { metric: 'Epochs', value: String(epochs) },
347
- { metric: 'Batch Size', value: String(batchSize) },
348
- { metric: 'Embedding Dim', value: String(dim) },
349
- { metric: 'Learning Rate', value: String(learningRate) },
350
- { metric: 'Patterns Recorded', value: patternsRecorded.toLocaleString() },
351
- { metric: 'Trajectories', value: String(trajectoriesCompleted) },
352
- { metric: 'Total Time', value: `${(totalTime / 1000).toFixed(1)}s` },
353
- { metric: 'Avg Epoch Time', value: `${(epochTimes.reduce((a, b) => a + b, 0) / epochTimes.length).toFixed(2)}ms` },
354
- ];
355
- // Native pipeline metrics (#2549 — the LoRA leg trained on ruvllm)
356
- if (nativeResult) {
357
- tableData.push({ metric: 'Backend', value: 'native (@ruvector/ruvllm TrainingPipeline)' }, { metric: 'Native Steps', value: String(nativeResult.steps) }, { metric: 'Final Loss', value: nativeResult.finalLoss.toExponential(3) });
358
- // Validation metrics only surface when a holdout actually ran
359
- // (bestValLoss is non-null); Early Stopped is only meaningful then.
360
- if (nativeResult.bestValLoss !== null && nativeResult.bestValLoss !== undefined) {
361
- tableData.push({ metric: 'Best Val Loss', value: nativeResult.bestValLoss.toExponential(3) }, { metric: 'Early Stopped', value: nativeResult.earlyStopped ? 'yes' : 'no' });
362
- }
363
- if (nativeResult.resumed) {
364
- tableData.push({
365
- metric: 'Resumed',
366
- value: nativeResult.resumeMode === 'resumeFrom'
367
- ? `${resumePath} (epoch position restored)`
368
- : `${resumePath} (weights only — epoch-position resume needs @ruvector/ruvllm >=2.6.0)`,
369
- });
370
- }
371
- if (nativeResult.checkpointPath) {
372
- tableData.push({
373
- metric: 'Checkpoint',
374
- value: `${nativeResult.checkpointPath}${nativeResult.checkpointBytes ? ` (${(nativeResult.checkpointBytes / 1024).toFixed(1)} KB)` : ''}`,
375
- });
376
- }
377
- }
378
- // Add WASM-specific metrics
379
- if (runWasmLeg && useWasm && wasmFeatures.length > 0) {
380
- const backendUsed = ruvectorStats?.backend || 'unknown';
381
- tableData.push({ metric: 'Backend', value: backendUsed === 'wasm' ? 'WASM (native)' : 'JS (fallback)' }, { metric: 'WASM Features', value: wasmFeatures.slice(0, 3).join(', ') }, { metric: 'LoRA Adaptations', value: String(adaptations) }, { metric: 'Avg Loss', value: (totalLoss / Math.max(1, epochs)).toFixed(4) });
382
- if (ruvectorStats?.microLoraStats) {
383
- tableData.push({ metric: 'MicroLoRA Delta Norm', value: ruvectorStats.microLoraStats.deltaNorm.toFixed(6) });
384
- }
385
- if (trajectoryStats) {
386
- tableData.push({ metric: 'Success Rate', value: `${(trajectoryStats.successRate * 100).toFixed(1)}%` }, { metric: 'Mean Improvement', value: `${(trajectoryStats.meanImprovement * 100).toFixed(1)}%` });
387
- }
388
- if (benchmark && benchmark.length > 0) {
389
- const flashBench = benchmark.find(b => b.name.includes('Flash'));
390
- if (flashBench) {
391
- tableData.push({ metric: 'Flash Attention', value: `${flashBench.opsPerSecond.toLocaleString()} ops/s` });
392
- }
393
- }
394
- }
395
- tableData.push({ metric: 'ReasoningBank Size', value: stats.reasoningBankSize.toLocaleString() }, { metric: 'Persisted To', value: output.dim(persistence.dataDir) });
396
- output.printTable({
397
- columns: [
398
- { key: 'metric', header: 'Metric', width: 26 },
399
- { key: 'value', header: 'Value', width: 32 },
400
- ],
401
- data: tableData,
402
- });
403
- output.writeln();
404
- output.writeln(output.success(`✓ ${patternsRecorded} patterns saved to ${persistence.patternsFile}`));
405
- if (useWasm && wasmFeatures.length > 0) {
406
- const backendUsed = ruvectorStats?.backend || 'unknown';
407
- const backendMsg = backendUsed === 'wasm'
408
- ? `RuVector WASM backend: ${wasmFeatures.join(', ')}`
409
- : `RuVector JS fallback (install @ruvector/learning-wasm for native speed): ${wasmFeatures.join(', ')}`;
410
- output.writeln(output.highlight(`✓ ${backendMsg}`));
411
- }
412
- return {
413
- success: true,
414
- data: {
415
- epochs,
416
- patternsRecorded,
417
- trajectoriesCompleted,
418
- totalTime,
419
- wasmFeatures,
420
- ruvectorStats,
421
- benchmark,
422
- stats,
423
- persistence
424
- }
425
- };
426
- }
427
- catch (error) {
428
- spinner.fail('Training failed');
429
- output.printError(error instanceof Error ? error.message : String(error));
430
- return { success: false, exitCode: 1 };
431
- }
432
- },
433
- };
434
- // Status subcommand - REAL measurements
435
- const statusCommand = {
436
- name: 'status',
437
- description: 'Check neural network status and loaded models',
438
- options: [
439
- { name: 'model', short: 'm', type: 'string', description: 'Specific model ID to check' },
440
- { name: 'verbose', short: 'v', type: 'boolean', description: 'Show detailed metrics' },
441
- ],
442
- examples: [
443
- { command: 'claude-flow neural status', description: 'Show all neural status' },
444
- { command: 'claude-flow neural status -m model-123', description: 'Check specific model' },
445
- ],
446
- action: async (ctx) => {
447
- const verbose = ctx.flags.verbose === true;
448
- output.writeln();
449
- output.writeln(output.bold('Neural Network Status (Real)'));
450
- output.writeln(output.dim('─'.repeat(50)));
451
- const spinner = output.createSpinner({ text: 'Checking neural systems...', spinner: 'dots' });
452
- spinner.start();
453
- try {
454
- // Import real implementations
455
- const { getIntelligenceStats, initializeIntelligence, benchmarkAdaptation } = await import('../memory/intelligence.js');
456
- const { getHNSWStatus, loadEmbeddingModel } = await import('../memory/memory-initializer.js');
457
- const ruvector = await import('../services/ruvector-training.js');
458
- // Initialize if needed and get real stats
459
- await initializeIntelligence();
460
- const stats = getIntelligenceStats();
461
- const hnswStatus = getHNSWStatus();
462
- // Quick benchmark for actual adaptation time
463
- const adaptBench = benchmarkAdaptation(100);
464
- // Check embedding model
465
- const modelInfo = await loadEmbeddingModel({ verbose: false });
466
- // Check RuVector WASM status
467
- const ruvectorStats = ruvector.getTrainingStats();
468
- const sonaAvailable = ruvector.isSonaAvailable();
469
- spinner.succeed('Neural systems checked');
470
- output.writeln();
471
- output.printTable({
472
- columns: [
473
- { key: 'component', header: 'Component', width: 22 },
474
- { key: 'status', header: 'Status', width: 12 },
475
- { key: 'details', header: 'Details', width: 32 },
476
- ],
477
- data: [
478
- {
479
- component: 'SONA Coordinator',
480
- status: stats.sonaEnabled ? output.success('Active') : output.warning('Inactive'),
481
- details: stats.sonaEnabled
482
- ? `Adaptation: ${(adaptBench.avgMs * 1000).toFixed(2)}μs avg`
483
- : 'Not initialized',
484
- },
485
- {
486
- component: 'RuVector Training',
487
- status: ruvectorStats.initialized ? output.success('Active') : output.dim('Not loaded'),
488
- details: ruvectorStats.initialized
489
- ? `${ruvectorStats.backend === 'wasm' ? 'WASM' : 'JS fallback'} | MicroLoRA: ${ruvectorStats.totalAdaptations} adapts`
490
- : 'Call neural train to initialize',
491
- },
492
- {
493
- component: 'SONA Engine',
494
- status: sonaAvailable ? output.success('Active') : output.dim('Not loaded'),
495
- details: sonaAvailable && ruvectorStats.sonaStats
496
- ? `${ruvectorStats.sonaStats.totalLearns} learns, ${ruvectorStats.sonaStats.totalSearches} searches`
497
- : 'Optional, enable with --sona',
498
- },
499
- {
500
- component: 'ReasoningBank',
501
- status: (stats.patternsLearned > 0 || stats.reasoningBankSize > 0)
502
- ? output.success('Active')
503
- : output.dim('Empty'),
504
- details: `${stats.patternsLearned} patterns stored`,
505
- },
506
- {
507
- // #2356: distinguish "loaded in this process" from "installed but
508
- // not yet loaded" from "not installed". Previously `neural status`
509
- // always printed "Not loaded" because it never warms the lazy
510
- // singleton — a false negative even when @ruvector/core is present.
511
- component: 'HNSW Index',
512
- status: hnswStatus.initialized
513
- ? output.success('Ready')
514
- : hnswStatus.available
515
- ? output.info('Available')
516
- : output.dim('Not installed'),
517
- details: hnswStatus.initialized
518
- ? `${hnswStatus.entryCount} vectors, ${hnswStatus.dimensions}-dim`
519
- : hnswStatus.available
520
- ? '@ruvector/core installed (loads on first vector search)'
521
- : '@ruvector/core not available',
522
- },
523
- {
524
- component: 'Embedding Model',
525
- status: modelInfo.success ? output.success('Loaded') : output.warning('Fallback'),
526
- details: `${modelInfo.modelName} (${modelInfo.dimensions}-dim)`,
527
- },
528
- {
529
- component: 'Flash Attention Ops',
530
- status: output.success('Available'),
531
- details: 'batchCosineSim, softmax, topK',
532
- },
533
- {
534
- component: 'Int8 Quantization',
535
- status: output.success('Available'),
536
- details: '~4x memory reduction',
537
- },
538
- {
539
- component: 'ruvllm Coordinator',
540
- status: stats._ruvllmBackend === 'active' ? output.success('Active') : output.dim('Unavailable'),
541
- details: stats._ruvllmBackend === 'active'
542
- ? `SonaCoordinator | ${stats._ruvllmTrajectories} trajectories`
543
- : 'Install @ruvector/ruvllm',
544
- },
545
- {
546
- component: 'Contrastive Trainer',
547
- // #2549 — three states: live session (object with counts),
548
- // 'available' (module resolves, no in-process session — the
549
- // normal case for a read-only status process), 'unavailable'
550
- // (module genuinely does not resolve).
551
- status: typeof stats._contrastiveTrainer === 'object'
552
- ? output.success('Active')
553
- : stats._contrastiveTrainer === 'available'
554
- ? output.success('Available')
555
- : output.dim('Unavailable'),
556
- details: typeof stats._contrastiveTrainer === 'object'
557
- ? `${stats._contrastiveTrainer.triplets ?? 0} triplets, ${stats._contrastiveTrainer.agents ?? 0} agents`
558
- : stats._contrastiveTrainer === 'available'
559
- ? 'ready — trains in-process on demand'
560
- : 'Install @ruvector/ruvllm',
561
- },
562
- {
563
- component: 'Training Pipeline',
564
- status: stats._trainingBackend === 'ruvllm' ? output.success('Available') : output.dim(stats._trainingBackend || 'Unavailable'),
565
- // Checkpoint capability is version-gated: saveCheckpoint(path)
566
- // was a silent no-op before @ruvector/ruvllm 2.5.7 (#2549).
567
- details: stats._trainingBackend === 'ruvllm'
568
- ? await (async () => {
569
- try {
570
- const { nativeCheckpointsSupported, latestCheckpointInfo } = await import('../ruvector/lora-adapter.js');
571
- // Most important info first (truncation-friendly): backend
572
- // capability, then the newest checkpoint + age when one exists.
573
- const base = nativeCheckpointsSupported()
574
- ? 'native @ruvector/ruvllm pipeline + disk checkpoints'
575
- : 'native @ruvector/ruvllm pipeline (checkpoints need >=2.5.7)';
576
- const cp = latestCheckpointInfo();
577
- return cp ? `${base} · latest: ${cp.filename} (${cp.ageLabel})` : base;
578
- }
579
- catch {
580
- return 'native @ruvector/ruvllm pipeline';
581
- }
582
- })()
583
- : 'JS fallback',
584
- },
585
- await (async () => {
586
- try {
587
- const { getGraphStats } = await import('../ruvector/graph-backend.js');
588
- const gs = await getGraphStats();
589
- return {
590
- component: 'Graph Database',
591
- status: gs.backend === 'graph-node' ? output.success('Active') : output.dim('Unavailable'),
592
- details: gs.backend === 'graph-node'
593
- ? `${gs.totalNodes} nodes, ${gs.totalEdges} edges`
594
- : 'Install @ruvector/graph-node',
595
- };
596
- }
597
- catch {
598
- return { component: 'Graph Database', status: output.dim('Unavailable'), details: 'Not loaded' };
599
- }
600
- })(),
601
- ],
602
- });
603
- if (verbose) {
604
- output.writeln();
605
- output.writeln(output.bold('Detailed Metrics'));
606
- const detailedData = [
607
- { metric: 'Trajectories Recorded', value: String(stats.trajectoriesRecorded) },
608
- { metric: 'Patterns Learned', value: String(stats.patternsLearned) },
609
- { metric: 'HNSW Dimensions', value: String(hnswStatus.dimensions) },
610
- { metric: 'SONA Adaptation (avg)', value: `${(adaptBench.avgMs * 1000).toFixed(2)}μs` },
611
- { metric: 'SONA Adaptation (max)', value: `${(adaptBench.maxMs * 1000).toFixed(2)}μs` },
612
- { metric: 'Target Met (<0.05ms)', value: adaptBench.targetMet ? output.success('Yes') : output.warning('No') },
613
- {
614
- metric: 'Last Adaptation',
615
- value: stats.lastAdaptation
616
- ? new Date(stats.lastAdaptation).toLocaleTimeString()
617
- : 'Never',
618
- },
619
- ];
620
- // Add RuVector WASM metrics if initialized
621
- if (ruvectorStats.initialized) {
622
- detailedData.push({ metric: 'RuVector Adaptations', value: String(ruvectorStats.totalAdaptations) }, { metric: 'RuVector Forwards', value: String(ruvectorStats.totalForwards) });
623
- if (ruvectorStats.microLoraStats) {
624
- detailedData.push({ metric: 'MicroLoRA Delta Norm', value: ruvectorStats.microLoraStats.deltaNorm.toFixed(6) }, { metric: 'MicroLoRA Adapt Count', value: String(ruvectorStats.microLoraStats.adaptCount) });
625
- }
626
- if (sonaAvailable && ruvectorStats.sonaStats?.stats) {
627
- const sonaStats = ruvectorStats.sonaStats.stats;
628
- detailedData.push({ metric: 'SONA Patterns Stored', value: String(sonaStats.patterns_stored || 0) }, { metric: 'SONA EWC Tasks', value: String(sonaStats.ewc_tasks || 0) });
629
- }
630
- }
631
- output.printTable({
632
- columns: [
633
- { key: 'metric', header: 'Metric', width: 28 },
634
- { key: 'value', header: 'Value', width: 20 },
635
- ],
636
- data: detailedData,
637
- });
638
- }
639
- return { success: true, data: { stats, hnswStatus, adaptBench, modelInfo, ruvectorStats } };
640
- }
641
- catch (error) {
642
- spinner.fail('Failed to check neural systems');
643
- output.printError(error instanceof Error ? error.message : String(error));
644
- return { success: false, exitCode: 1 };
645
- }
646
- },
647
- };
648
- // Patterns subcommand
649
- const patternsCommand = {
650
- name: 'patterns',
651
- description: 'Analyze and manage cognitive patterns',
652
- options: [
653
- { name: 'action', short: 'a', type: 'string', description: 'Action: analyze, learn, predict, list', default: 'list' },
654
- { name: 'query', short: 'q', type: 'string', description: 'Pattern query for search' },
655
- { name: 'limit', short: 'l', type: 'number', description: 'Max patterns to return', default: '10' },
656
- ],
657
- examples: [
658
- { command: 'claude-flow neural patterns --action list', description: 'List all patterns' },
659
- { command: 'claude-flow neural patterns -a analyze -q "error handling"', description: 'Analyze patterns' },
660
- ],
661
- action: async (ctx) => {
662
- const action = ctx.flags.action || 'list';
663
- const query = ctx.flags.query;
664
- const limit = parseInt(ctx.flags.limit, 10) || 10;
665
- output.writeln();
666
- output.writeln(output.bold(`Neural Patterns - ${action}`));
667
- output.writeln(output.dim('─'.repeat(40)));
668
- try {
669
- const { initializeIntelligence, getIntelligenceStats, findSimilarPatterns, getAllPatterns, getPersistenceStatus, } = await import('../memory/intelligence.js');
670
- await initializeIntelligence();
671
- const stats = getIntelligenceStats();
672
- const persistence = getPersistenceStatus();
673
- if (action === 'list') {
674
- // Get ALL patterns from ReasoningBank (loaded from disk)
675
- const allPatterns = await getAllPatterns();
676
- const patterns = query
677
- ? await findSimilarPatterns(query, { k: limit })
678
- : allPatterns.slice(0, limit);
679
- if (patterns.length === 0) {
680
- output.writeln(output.dim('No patterns found. Train some patterns first with: neural train'));
681
- output.writeln();
682
- output.printBox([
683
- `Total Patterns: ${stats.patternsLearned}`,
684
- `Trajectories: ${stats.trajectoriesRecorded}`,
685
- `ReasoningBank Size: ${stats.reasoningBankSize}`,
686
- `Persistence: ${persistence.patternsExist ? 'Loaded from disk' : 'Not persisted'}`,
687
- `Data Dir: ${persistence.dataDir}`,
688
- ].join('\n'), 'Pattern Statistics');
689
- }
690
- else {
691
- output.printTable({
692
- columns: [
693
- { key: 'id', header: 'ID', width: 20 },
694
- { key: 'type', header: 'Type', width: 18 },
695
- { key: 'confidence', header: 'Confidence', width: 12 },
696
- { key: 'usage', header: 'Usage', width: 10 },
697
- ],
698
- data: patterns.map((p, i) => ({
699
- id: (p.id || `P${String(i + 1).padStart(3, '0')}`).substring(0, 18),
700
- type: output.highlight(p.type || 'unknown'),
701
- confidence: `${((p.confidence || 0.5) * 100).toFixed(1)}%`,
702
- usage: String(p.usageCount || 0),
703
- })),
704
- });
705
- }
706
- output.writeln();
707
- output.writeln(output.dim(`Total: ${allPatterns.length} patterns (persisted) | Trajectories: ${stats.trajectoriesRecorded}`));
708
- if (persistence.patternsExist) {
709
- output.writeln(output.success(`✓ Loaded from: ${persistence.patternsFile}`));
710
- }
711
- }
712
- else if (action === 'analyze' && query) {
713
- // Analyze patterns related to query
714
- const related = await findSimilarPatterns(query, { k: limit });
715
- output.writeln(`Analyzing patterns related to: "${query}"`);
716
- output.writeln();
717
- if (related.length > 0) {
718
- output.printTable({
719
- columns: [
720
- { key: 'content', header: 'Pattern', width: 40 },
721
- { key: 'confidence', header: 'Confidence', width: 12 },
722
- { key: 'type', header: 'Type', width: 15 },
723
- ],
724
- data: related.slice(0, 5).map(p => ({
725
- content: (p.content || '').substring(0, 38) + (p.content?.length > 38 ? '...' : ''),
726
- confidence: `${((p.confidence || 0) * 100).toFixed(0)}%`,
727
- type: p.type || 'general',
728
- })),
729
- });
730
- }
731
- else {
732
- output.writeln(output.dim('No related patterns found.'));
733
- }
734
- }
735
- return { success: true };
736
- }
737
- catch (error) {
738
- // Fallback if intelligence not initialized
739
- output.writeln(output.dim('Intelligence system not initialized.'));
740
- output.writeln(output.dim('Run: claude-flow neural train --pattern-type general'));
741
- return { success: false };
742
- }
743
- },
744
- };
745
- // Predict subcommand
746
- const predictCommand = {
747
- name: 'predict',
748
- description: 'Make AI predictions using trained models',
749
- options: [
750
- { name: 'input', short: 'i', type: 'string', description: 'Input text to predict routing for', required: true },
751
- { name: 'k', short: 'k', type: 'number', description: 'Number of top predictions', default: '5' },
752
- { name: 'format', short: 'f', type: 'string', description: 'Output format: json, table', default: 'table' },
753
- ],
754
- examples: [
755
- { command: 'claude-flow neural predict -i "implement authentication"', description: 'Predict routing for task' },
756
- { command: 'claude-flow neural predict -i "fix bug in login" -k 3', description: 'Get top 3 predictions' },
757
- ],
758
- action: async (ctx) => {
759
- const input = ctx.flags.input;
760
- const k = parseInt(ctx.flags.k || '5', 10);
761
- const format = ctx.flags.format || 'table';
762
- if (!input) {
763
- output.printError('--input is required');
764
- return { success: false, exitCode: 1 };
765
- }
766
- output.writeln();
767
- output.writeln(output.bold('Neural Prediction (Real)'));
768
- output.writeln(output.dim('─'.repeat(50)));
769
- const spinner = output.createSpinner({ text: 'Running inference...', spinner: 'dots' });
770
- spinner.start();
771
- try {
772
- const { initializeIntelligence, findSimilarPatterns } = await import('../memory/intelligence.js');
773
- // Initialize intelligence system
774
- await initializeIntelligence();
775
- // Find similar patterns (embedding is done internally)
776
- const startSearch = performance.now();
777
- const matches = await findSimilarPatterns(input, { k });
778
- const searchTime = performance.now() - startSearch;
779
- spinner.succeed(`Prediction complete (search: ${searchTime.toFixed(1)}ms)`);
780
- output.writeln();
781
- if (matches.length === 0) {
782
- output.writeln(output.warning('No similar patterns found. Try training first: claude-flow neural train'));
783
- return { success: true, data: { matches: [] } };
784
- }
785
- if (format === 'json') {
786
- output.writeln(JSON.stringify(matches, null, 2));
787
- }
788
- else {
789
- // Determine best prediction based on patterns
790
- const patternTypes = {};
791
- for (const match of matches) {
792
- const type = match.type || 'unknown';
793
- patternTypes[type] = (patternTypes[type] || 0) + match.similarity;
794
- }
795
- const sorted = Object.entries(patternTypes).sort((a, b) => b[1] - a[1]);
796
- const topType = sorted[0]?.[0] || 'unknown';
797
- const confidence = matches[0]?.similarity || 0;
798
- output.printBox([
799
- `Input: ${input.substring(0, 60)}${input.length > 60 ? '...' : ''}`,
800
- ``,
801
- `Predicted Type: ${topType}`,
802
- `Confidence: ${(confidence * 100).toFixed(1)}%`,
803
- `Latency: ${searchTime.toFixed(1)}ms`,
804
- ``,
805
- `Top ${matches.length} Similar Patterns:`,
806
- ].join('\n'), 'Result');
807
- output.printTable({
808
- columns: [
809
- { key: 'rank', header: '#', width: 3 },
810
- { key: 'id', header: 'Pattern ID', width: 20 },
811
- { key: 'type', header: 'Type', width: 15 },
812
- { key: 'similarity', header: 'Similarity', width: 12 },
813
- ],
814
- data: matches.slice(0, k).map((m, i) => ({
815
- rank: String(i + 1),
816
- id: m.id?.substring(0, 20) || 'unknown',
817
- type: m.type || 'action',
818
- similarity: `${(m.similarity * 100).toFixed(1)}%`,
819
- })),
820
- });
821
- }
822
- return { success: true, data: { matches, searchTime } };
823
- }
824
- catch (error) {
825
- spinner.fail('Prediction failed');
826
- output.printError(error instanceof Error ? error.message : String(error));
827
- return { success: false, exitCode: 1 };
828
- }
829
- },
830
- };
831
- // Optimize subcommand - Real Int8 quantization and pattern optimization
832
- const optimizeCommand = {
833
- name: 'optimize',
834
- description: 'Optimize neural patterns (Int8 quantization, memory compression)',
835
- options: [
836
- { name: 'method', type: 'string', description: 'Method: quantize, analyze, compact', default: 'quantize' },
837
- { name: 'verbose', short: 'v', type: 'boolean', description: 'Show detailed metrics' },
838
- ],
839
- examples: [
840
- { command: 'claude-flow neural optimize --method quantize', description: 'Quantize patterns to Int8' },
841
- { command: 'claude-flow neural optimize --method analyze -v', description: 'Analyze memory usage' },
842
- ],
843
- action: async (ctx) => {
844
- const method = ctx.flags.method || 'quantize';
845
- const verbose = ctx.flags.verbose === true;
846
- output.writeln();
847
- output.writeln(output.bold('Pattern Optimization (Real)'));
848
- output.writeln(output.dim('─'.repeat(50)));
849
- const spinner = output.createSpinner({ text: `Running ${method} optimization...`, spinner: 'dots' });
850
- spinner.start();
851
- try {
852
- const { initializeIntelligence, getIntelligenceStats, getAllPatterns, flushPatterns, compactPatterns } = await import('../memory/intelligence.js');
853
- const fs = await import('fs');
854
- const path = await import('path');
855
- await initializeIntelligence();
856
- const patterns = await getAllPatterns();
857
- const stats = getIntelligenceStats();
858
- // Trigger ruvllm background learning if available
859
- try {
860
- const { runBackgroundLearning } = await import('../memory/intelligence.js');
861
- await runBackgroundLearning();
862
- }
863
- catch { /* background learning is best-effort */ }
864
- // Get actual pattern storage size
865
- const patternDir = path.join(process.cwd(), '.claude-flow', 'neural');
866
- let beforeSize = 0;
867
- try {
868
- const patternFile = path.join(patternDir, 'patterns.json');
869
- if (fs.existsSync(patternFile)) {
870
- beforeSize = fs.statSync(patternFile).size;
871
- }
872
- }
873
- catch { /* ignore */ }
874
- if (method === 'quantize') {
875
- // Perform real Int8 quantization on pattern embeddings
876
- spinner.setText('Quantizing pattern embeddings to Int8...');
877
- let quantizedCount = 0;
878
- let totalBeforeValues = 0;
879
- let totalAfterValues = 0;
880
- for (const pattern of patterns) {
881
- if (pattern.embedding && pattern.embedding.length > 0) {
882
- totalBeforeValues += pattern.embedding.length;
883
- // Actually quantize: scale Float32 values to Int8 range [-128, 127]
884
- const emb = pattern.embedding;
885
- let min = Infinity, max = -Infinity;
886
- for (const v of emb) {
887
- if (v < min)
888
- min = v;
889
- if (v > max)
890
- max = v;
891
- }
892
- const range = max - min || 1;
893
- const scale = 255 / range;
894
- const offset = min;
895
- // Convert in-place to quantized integer values
896
- for (let i = 0; i < emb.length; i++) {
897
- emb[i] = Math.round((emb[i] - offset) * scale) - 128;
898
- }
899
- // Store quantization params for dequantization (extra fields survive JSON serialization)
900
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
901
- const p = pattern;
902
- p.quantized = true;
903
- p.quantScale = scale;
904
- p.quantOffset = offset;
905
- totalAfterValues += pattern.embedding.length;
906
- quantizedCount++;
907
- }
908
- }
909
- // Save actually-quantized patterns (integers serialize smaller in JSON)
910
- await flushPatterns();
911
- // Measure real file size after quantization
912
- let afterSize = beforeSize;
913
- try {
914
- const patternFile = path.join(patternDir, 'patterns.json');
915
- if (fs.existsSync(patternFile)) {
916
- afterSize = fs.statSync(patternFile).size;
917
- }
918
- }
919
- catch { /* ignore */ }
920
- const actualRatio = beforeSize > 0 && afterSize > 0 ? (beforeSize / afterSize) : 0;
921
- spinner.succeed(`Quantized ${quantizedCount} pattern embeddings to Int8`);
922
- output.writeln();
923
- output.printTable({
924
- columns: [
925
- { key: 'metric', header: 'Metric', width: 25 },
926
- { key: 'before', header: 'Before', width: 18 },
927
- { key: 'after', header: 'After', width: 18 },
928
- ],
929
- data: [
930
- { metric: 'Pattern Count', before: String(patterns.length), after: String(patterns.length) },
931
- { metric: 'Quantized', before: '-', after: String(quantizedCount) },
932
- { metric: 'Storage Size', before: `${(beforeSize / 1024).toFixed(1)} KB`, after: `${(afterSize / 1024).toFixed(1)} KB` },
933
- { metric: 'Reduction Ratio', before: '-', after: actualRatio > 0 ? `${actualRatio.toFixed(2)}x` : 'N/A (no data)' },
934
- { metric: 'Precision', before: 'Float32', after: 'Int8 (±0.5%)' },
935
- ],
936
- });
937
- }
938
- else if (method === 'analyze') {
939
- spinner.succeed('Analysis complete');
940
- output.writeln();
941
- output.writeln(output.bold('Pattern Memory Analysis'));
942
- const embeddingBytes = patterns.reduce((sum, p) => sum + (p.embedding?.length || 0) * 4, 0);
943
- const metadataEstimate = patterns.length * 100; // ~100 bytes per pattern metadata
944
- output.printTable({
945
- columns: [
946
- { key: 'component', header: 'Component', width: 25 },
947
- { key: 'size', header: 'Size', width: 18 },
948
- { key: 'count', header: 'Count', width: 12 },
949
- ],
950
- data: [
951
- { component: 'Pattern Embeddings (F32)', size: `${(embeddingBytes / 1024).toFixed(1)} KB`, count: String(patterns.length) },
952
- { component: 'Pattern Metadata', size: `${(metadataEstimate / 1024).toFixed(1)} KB`, count: '-' },
953
- { component: 'Total In-Memory', size: `${((embeddingBytes + metadataEstimate) / 1024).toFixed(1)} KB`, count: '-' },
954
- { component: 'Storage (patterns.json)', size: `${(beforeSize / 1024).toFixed(1)} KB`, count: '-' },
955
- { component: 'Trajectories', size: '-', count: String(stats.trajectoriesRecorded) },
956
- ],
957
- });
958
- if (verbose) {
959
- output.writeln();
960
- output.writeln(output.bold('Optimization Recommendations'));
961
- const recommendations = [];
962
- if (patterns.length > 1000) {
963
- recommendations.push('- Consider pruning low-usage patterns');
964
- }
965
- if (embeddingBytes > 1024 * 1024) {
966
- recommendations.push('- Int8 quantization would reduce memory by ~75%');
967
- }
968
- if (stats.trajectoriesRecorded > 100) {
969
- recommendations.push('- Trajectory consolidation available');
970
- }
971
- if (recommendations.length === 0) {
972
- recommendations.push('- Patterns are already well optimized');
973
- }
974
- recommendations.forEach(r => output.writeln(r));
975
- }
976
- }
977
- else if (method === 'compact') {
978
- spinner.setText('Compacting pattern storage...');
979
- // Remove duplicate or very similar patterns
980
- const compacted = await compactPatterns(0.95); // Remove patterns with >95% similarity
981
- spinner.succeed(`Compacted ${compacted.removed} patterns`);
982
- output.writeln();
983
- output.printTable({
984
- columns: [
985
- { key: 'metric', header: 'Metric', width: 20 },
986
- { key: 'value', header: 'Value', width: 15 },
987
- ],
988
- data: [
989
- { metric: 'Patterns Before', value: String(compacted.before) },
990
- { metric: 'Patterns After', value: String(compacted.after) },
991
- { metric: 'Removed', value: String(compacted.removed) },
992
- { metric: 'Similarity Threshold', value: '95%' },
993
- ],
994
- });
995
- }
996
- return { success: true };
997
- }
998
- catch (error) {
999
- spinner.fail('Optimization failed');
1000
- output.printError(error instanceof Error ? error.message : String(error));
1001
- return { success: false, exitCode: 1 };
1002
- }
1003
- },
1004
- };
1005
- // Export subcommand - Securely export trained models to IPFS
1006
- const exportCommand = {
1007
- name: 'export',
1008
- description: 'Export trained models to IPFS for sharing (Ed25519 signed)',
1009
- options: [
1010
- { name: 'model', short: 'm', type: 'string', description: 'Model ID or category to export' },
1011
- { name: 'output', short: 'o', type: 'string', description: 'Output file path (optional)' },
1012
- { name: 'ipfs', short: 'i', type: 'boolean', description: 'Pin to IPFS (requires Pinata credentials)' },
1013
- { name: 'sign', short: 's', type: 'boolean', description: 'Sign with Ed25519 key', default: 'true' },
1014
- { name: 'strip-pii', type: 'boolean', description: 'Strip potential PII from export', default: 'true' },
1015
- { name: 'name', short: 'n', type: 'string', description: 'Custom name for exported model' },
1016
- ],
1017
- examples: [
1018
- { command: 'claude-flow neural export -m security-patterns --ipfs', description: 'Export and pin to IPFS' },
1019
- { command: 'claude-flow neural export -m code-review -o ./export.json', description: 'Export to file' },
1020
- ],
1021
- action: async (ctx) => {
1022
- const modelId = ctx.flags.model || 'all';
1023
- const outputFile = ctx.flags.output;
1024
- const pinToIpfs = ctx.flags.ipfs;
1025
- const signExport = ctx.flags.sign !== false;
1026
- const stripPii = ctx.flags['strip-pii'] !== false;
1027
- const customName = ctx.flags.name;
1028
- output.writeln();
1029
- output.writeln(output.bold('Secure Model Export'));
1030
- output.writeln(output.dim('─'.repeat(50)));
1031
- const spinner = output.createSpinner({ text: 'Preparing export...', spinner: 'dots' });
1032
- spinner.start();
1033
- try {
1034
- const fs = await import('fs');
1035
- const path = await import('path');
1036
- const crypto = await import('crypto');
1037
- // Collect trained patterns from memory
1038
- spinner.setText('Collecting trained patterns...');
1039
- const { getIntelligenceStats, flushPatterns } = await import('../memory/intelligence.js');
1040
- await flushPatterns(); // Ensure all patterns are persisted
1041
- const stats = await getIntelligenceStats();
1042
- // SECURITY: Build export data - NEVER include secrets
1043
- // - API keys read from env but NEVER included in export
1044
- // - Uses ephemeral signing keys (generated per-export, not stored)
1045
- // - PII stripping enabled by default
1046
- // - Suspicious pattern content blocked
1047
- const exportData = {
1048
- type: 'learning-pattern',
1049
- version: '1.0.0',
1050
- name: customName || `claude-flow-model-${Date.now()}`,
1051
- exportedAt: new Date().toISOString(),
1052
- modelId,
1053
- patterns: [],
1054
- metadata: {
1055
- sourceVersion: '3.0.0-alpha',
1056
- piiStripped: stripPii,
1057
- signed: signExport,
1058
- accuracy: 0,
1059
- totalUsage: 0,
1060
- },
1061
- };
1062
- // Load patterns from local storage
1063
- const memoryDir = path.join(process.cwd(), '.claude-flow', 'memory');
1064
- const patternsFile = path.join(memoryDir, 'patterns.json');
1065
- if (fs.existsSync(patternsFile)) {
1066
- const patterns = JSON.parse(fs.readFileSync(patternsFile, 'utf8'));
1067
- for (const pattern of patterns) {
1068
- // Security: Strip potential PII
1069
- if (stripPii) {
1070
- // Remove any paths, usernames, or sensitive data
1071
- if (pattern.content) {
1072
- pattern.content = pattern.content
1073
- .replace(/\/Users\/[^\/]+/g, '/Users/[REDACTED]')
1074
- .replace(/\/home\/[^\/]+/g, '/home/[REDACTED]')
1075
- .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL_REDACTED]')
1076
- .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[IP_REDACTED]');
1077
- }
1078
- }
1079
- exportData.patterns.push({
1080
- id: pattern.id || crypto.randomBytes(8).toString('hex'),
1081
- trigger: pattern.trigger || pattern.type || 'general',
1082
- action: pattern.action || pattern.recommendation || 'apply-pattern',
1083
- confidence: pattern.confidence || 0.85,
1084
- usageCount: pattern.usageCount || 1,
1085
- });
1086
- }
1087
- }
1088
- // Add stats metadata
1089
- exportData.metadata.accuracy = stats.retrievalPrecision || 0.85;
1090
- exportData.metadata.totalUsage = exportData.patterns.reduce((sum, p) => sum + p.usageCount, 0);
1091
- spinner.setText('Generating secure signature...');
1092
- // Sign with Ed25519 if requested
1093
- let signature = null;
1094
- let publicKey = null;
1095
- if (signExport) {
1096
- // Generate ephemeral key pair for signing
1097
- // Use Node.js webcrypto for Ed25519 signing
1098
- const { webcrypto } = crypto;
1099
- const keyPair = await webcrypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']
1100
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1101
- );
1102
- const exportBytes = new TextEncoder().encode(JSON.stringify(exportData));
1103
- const signatureBytes = await webcrypto.subtle.sign('Ed25519', keyPair.privateKey, exportBytes);
1104
- signature = Buffer.from(signatureBytes).toString('hex');
1105
- const publicKeyBytes = await webcrypto.subtle.exportKey('raw', keyPair.publicKey);
1106
- publicKey = Buffer.from(publicKeyBytes).toString('hex');
1107
- }
1108
- // SECURITY: Final export package - verify no secrets leaked
1109
- const exportPackage = {
1110
- pinataContent: exportData,
1111
- pinataMetadata: {
1112
- name: exportData.name,
1113
- keyvalues: {
1114
- type: 'learning-pattern',
1115
- version: '1.0.0',
1116
- signed: signExport ? 'true' : 'false',
1117
- },
1118
- },
1119
- signature,
1120
- publicKey: publicKey ? `ed25519:${publicKey}` : null,
1121
- // Note: Private key is ephemeral and NEVER stored or exported
1122
- };
1123
- // SECURITY AUDIT: Ensure no secrets in export
1124
- const exportStr = JSON.stringify(exportPackage);
1125
- const secretPatterns = [
1126
- /sk-ant-[a-zA-Z0-9-]+/, // Anthropic keys
1127
- /sk-[a-zA-Z0-9]{48}/, // OpenAI keys
1128
- /AIza[a-zA-Z0-9-_]{35}/, // Google keys
1129
- /pinata_[a-zA-Z0-9]+/, // Pinata JWT
1130
- /-----BEGIN.*KEY-----/, // PEM keys
1131
- ];
1132
- for (const pattern of secretPatterns) {
1133
- if (pattern.test(exportStr)) {
1134
- spinner.fail('SECURITY: Export contains potential API keys - aborting');
1135
- return { success: false, exitCode: 1 };
1136
- }
1137
- }
1138
- // Output handling
1139
- if (outputFile) {
1140
- fs.writeFileSync(outputFile, JSON.stringify(exportPackage, null, 2));
1141
- spinner.succeed(`Exported to: ${outputFile}`);
1142
- }
1143
- if (pinToIpfs) {
1144
- spinner.setText('Pinning to IPFS...');
1145
- // Check for Pinata credentials
1146
- const pinataKey = process.env.PINATA_API_KEY;
1147
- const pinataSecret = process.env.PINATA_API_SECRET;
1148
- if (!pinataKey || !pinataSecret) {
1149
- spinner.fail('PINATA_API_KEY and PINATA_API_SECRET required for IPFS export');
1150
- output.writeln(output.dim('Set these in your environment or .env file'));
1151
- return { success: false, exitCode: 1 };
1152
- }
1153
- const response = await fetch('https://api.pinata.cloud/pinning/pinJSONToIPFS', {
1154
- method: 'POST',
1155
- headers: {
1156
- 'Content-Type': 'application/json',
1157
- 'pinata_api_key': pinataKey,
1158
- 'pinata_secret_api_key': pinataSecret,
1159
- },
1160
- body: JSON.stringify(exportPackage),
1161
- });
1162
- if (!response.ok) {
1163
- const error = await response.text();
1164
- spinner.fail(`IPFS pin failed: ${error}`);
1165
- return { success: false, exitCode: 1 };
1166
- }
1167
- const result = await response.json();
1168
- spinner.succeed('Successfully exported to IPFS');
1169
- output.writeln();
1170
- output.table({
1171
- columns: [
1172
- { key: 'property', header: 'Property', width: 20 },
1173
- { key: 'value', header: 'Value', width: 50 },
1174
- ],
1175
- data: [
1176
- { property: 'CID', value: result.IpfsHash },
1177
- { property: 'Size', value: `${result.PinSize} bytes` },
1178
- { property: 'Gateway URL', value: `https://gateway.pinata.cloud/ipfs/${result.IpfsHash}` },
1179
- { property: 'Patterns', value: String(exportData.patterns.length) },
1180
- { property: 'Signed', value: signExport ? 'Yes (Ed25519)' : 'No' },
1181
- { property: 'PII Stripped', value: stripPii ? 'Yes' : 'No' },
1182
- ],
1183
- });
1184
- output.writeln();
1185
- output.writeln(output.success('Share this CID for others to import your trained patterns'));
1186
- output.writeln(output.dim(`Import command: claude-flow neural import --cid ${result.IpfsHash}`));
1187
- }
1188
- if (!outputFile && !pinToIpfs) {
1189
- // Just display the export
1190
- spinner.succeed('Export prepared');
1191
- output.writeln();
1192
- output.writeln(JSON.stringify(exportPackage, null, 2));
1193
- }
1194
- return { success: true };
1195
- }
1196
- catch (error) {
1197
- spinner.fail(`Export failed: ${error instanceof Error ? error.message : String(error)}`);
1198
- return { success: false, exitCode: 1 };
1199
- }
1200
- },
1201
- };
1202
- // List subcommand - List available pre-trained models
1203
- const listCommand = {
1204
- name: 'list',
1205
- description: 'List available pre-trained models from the official registry',
1206
- options: [
1207
- { name: 'category', type: 'string', description: 'Filter by category (security, quality, performance, etc.)' },
1208
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json, simple', default: 'table' },
1209
- { name: 'cid', type: 'string', description: 'Custom registry CID (default: official registry)' },
1210
- ],
1211
- examples: [
1212
- { command: 'claude-flow neural list', description: 'List all available models' },
1213
- { command: 'claude-flow neural list --category security', description: 'List only security models' },
1214
- { command: 'claude-flow neural list -f json', description: 'Output as JSON' },
1215
- ],
1216
- action: async (ctx) => {
1217
- const category = ctx.flags.category;
1218
- const format = ctx.flags.format || 'table';
1219
- const customCid = ctx.flags.cid;
1220
- // Official model registry CID
1221
- const registryCid = customCid || 'QmNr1yYMKi7YBaL8JSztQyuB5ZUaTdRMLxJC1pBpGbjsTc';
1222
- output.writeln();
1223
- output.writeln(output.bold('Pre-trained Model Registry'));
1224
- output.writeln(output.dim('─'.repeat(60)));
1225
- const spinner = output.createSpinner({ text: 'Fetching model registry...', spinner: 'dots' });
1226
- spinner.start();
1227
- try {
1228
- const gateways = [
1229
- 'https://gateway.pinata.cloud',
1230
- 'https://ipfs.io',
1231
- 'https://dweb.link',
1232
- ];
1233
- let registry = null;
1234
- for (const gateway of gateways) {
1235
- try {
1236
- const response = await fetch(`${gateway}/ipfs/${registryCid}`, {
1237
- signal: AbortSignal.timeout(15000),
1238
- headers: { 'Accept': 'application/json' },
1239
- });
1240
- if (response.ok) {
1241
- registry = await response.json();
1242
- break;
1243
- }
1244
- }
1245
- catch {
1246
- continue;
1247
- }
1248
- }
1249
- if (!registry || !registry.models) {
1250
- spinner.fail('Could not fetch model registry');
1251
- return { success: false, exitCode: 1 };
1252
- }
1253
- const registryData = registry;
1254
- // Filter by category if specified
1255
- let models = registryData.models;
1256
- if (category) {
1257
- models = models.filter(m => m.category === category ||
1258
- m.id.includes(category) ||
1259
- m.name.toLowerCase().includes(category.toLowerCase()));
1260
- spinner.succeed(`Found ${models.length} models matching "${category}"`);
1261
- }
1262
- else {
1263
- spinner.succeed(`Found ${registryData.models.length} models`);
1264
- }
1265
- if (models.length === 0) {
1266
- output.writeln(output.warning(`No models found for category: ${category}`));
1267
- output.writeln(output.dim('Available categories: security, quality, performance, testing, api, debugging, refactoring, documentation'));
1268
- return { success: false, exitCode: 1 };
1269
- }
1270
- output.writeln();
1271
- if (format === 'json') {
1272
- output.writeln(JSON.stringify(models, null, 2));
1273
- }
1274
- else if (format === 'simple') {
1275
- for (const model of models) {
1276
- output.writeln(`${model.id} (${model.category}) - ${model.patterns.length} patterns, ${(model.metadata.accuracy * 100).toFixed(0)}% accuracy`);
1277
- }
1278
- }
1279
- else {
1280
- // Table format
1281
- output.printTable({
1282
- columns: [
1283
- { key: 'id', header: 'Model ID', width: 35 },
1284
- { key: 'category', header: 'Category', width: 14 },
1285
- { key: 'patterns', header: 'Patterns', width: 10 },
1286
- { key: 'accuracy', header: 'Accuracy', width: 10 },
1287
- { key: 'usage', header: 'Usage', width: 10 },
1288
- ],
1289
- data: models.map(m => ({
1290
- id: m.id,
1291
- category: m.category,
1292
- patterns: String(m.patterns.length),
1293
- accuracy: `${(m.metadata.accuracy * 100).toFixed(0)}%`,
1294
- usage: m.metadata.totalUsage.toLocaleString(),
1295
- })),
1296
- });
1297
- output.writeln();
1298
- output.writeln(output.dim('Registry CID: ' + registryCid));
1299
- output.writeln();
1300
- output.writeln(output.bold('Import Commands:'));
1301
- output.writeln(output.dim(' All models: ') + `claude-flow neural import --cid ${registryCid}`);
1302
- if (category) {
1303
- output.writeln(output.dim(` ${category} only: `) + `claude-flow neural import --cid ${registryCid} --category ${category}`);
1304
- }
1305
- else {
1306
- output.writeln(output.dim(' By category: ') + `claude-flow neural import --cid ${registryCid} --category <category>`);
1307
- }
1308
- }
1309
- return { success: true };
1310
- }
1311
- catch (error) {
1312
- spinner.fail(`Failed to list models: ${error instanceof Error ? error.message : String(error)}`);
1313
- return { success: false, exitCode: 1 };
1314
- }
1315
- },
1316
- };
1317
- // Import subcommand - Securely import models from IPFS
1318
- const importCommand = {
1319
- name: 'import',
1320
- description: 'Import trained models from IPFS with signature verification',
1321
- options: [
1322
- { name: 'cid', short: 'c', type: 'string', description: 'IPFS CID to import from' },
1323
- { name: 'file', short: 'f', type: 'string', description: 'Local file to import' },
1324
- { name: 'verify', short: 'v', type: 'boolean', description: 'Verify Ed25519 signature', default: 'true' },
1325
- { name: 'merge', type: 'boolean', description: 'Merge with existing patterns (vs replace)', default: 'true' },
1326
- { name: 'category', type: 'string', description: 'Only import patterns from specific category' },
1327
- ],
1328
- examples: [
1329
- { command: 'claude-flow neural import --cid QmXxx...', description: 'Import from IPFS' },
1330
- { command: 'claude-flow neural import -f ./patterns.json --verify', description: 'Import from file' },
1331
- { command: 'claude-flow neural import --cid QmNr1yYMK... --category security', description: 'Import only security patterns' },
1332
- ],
1333
- action: async (ctx) => {
1334
- const cid = ctx.flags.cid;
1335
- const file = ctx.flags.file;
1336
- const verifySignature = ctx.flags.verify !== false;
1337
- const merge = ctx.flags.merge !== false;
1338
- const categoryFilter = ctx.flags.category;
1339
- if (!cid && !file) {
1340
- output.writeln(output.error('Either --cid or --file is required'));
1341
- return { success: false, exitCode: 1 };
1342
- }
1343
- output.writeln();
1344
- output.writeln(output.bold('Secure Model Import'));
1345
- output.writeln(output.dim('─'.repeat(50)));
1346
- const spinner = output.createSpinner({ text: 'Fetching model...', spinner: 'dots' });
1347
- spinner.start();
1348
- try {
1349
- const fs = await import('fs');
1350
- const path = await import('path');
1351
- const crypto = await import('crypto');
1352
- let importData = null;
1353
- // Fetch from IPFS or file
1354
- if (cid) {
1355
- const gateways = [
1356
- 'https://gateway.pinata.cloud',
1357
- 'https://ipfs.io',
1358
- 'https://dweb.link',
1359
- ];
1360
- for (const gateway of gateways) {
1361
- try {
1362
- spinner.setText(`Fetching from ${gateway}...`);
1363
- const response = await fetch(`${gateway}/ipfs/${cid}`, {
1364
- signal: AbortSignal.timeout(30000),
1365
- headers: { 'Accept': 'application/json' },
1366
- });
1367
- if (response.ok) {
1368
- importData = await response.json();
1369
- break;
1370
- }
1371
- }
1372
- catch {
1373
- continue;
1374
- }
1375
- }
1376
- if (!importData) {
1377
- spinner.fail('Could not fetch from any IPFS gateway');
1378
- return { success: false, exitCode: 1 };
1379
- }
1380
- }
1381
- else {
1382
- if (!fs.existsSync(file)) {
1383
- spinner.fail(`File not found: ${file}`);
1384
- return { success: false, exitCode: 1 };
1385
- }
1386
- importData = JSON.parse(fs.readFileSync(file, 'utf8'));
1387
- }
1388
- if (!importData) {
1389
- spinner.fail('No import data available');
1390
- return { success: false, exitCode: 1 };
1391
- }
1392
- // Verify signature if present and requested
1393
- if (verifySignature && importData.signature && importData.publicKey) {
1394
- spinner.setText('Verifying Ed25519 signature...');
1395
- try {
1396
- const { webcrypto } = crypto;
1397
- const publicKeyHex = importData.publicKey.replace('ed25519:', '');
1398
- const publicKeyBytes = Buffer.from(publicKeyHex, 'hex');
1399
- const signatureBytes = Buffer.from(importData.signature, 'hex');
1400
- const publicKey = await webcrypto.subtle.importKey('raw', publicKeyBytes, { name: 'Ed25519' }, false, ['verify']);
1401
- const dataBytes = new TextEncoder().encode(JSON.stringify(importData.pinataContent));
1402
- const valid = await webcrypto.subtle.verify('Ed25519', publicKey, signatureBytes, dataBytes);
1403
- if (!valid) {
1404
- spinner.fail('Signature verification FAILED - data may be tampered');
1405
- return { success: false, exitCode: 1 };
1406
- }
1407
- output.writeln(output.success('Signature verified'));
1408
- }
1409
- catch (err) {
1410
- output.writeln(output.warning(`Signature verification skipped: ${err instanceof Error ? err.message : String(err)}`));
1411
- }
1412
- }
1413
- // Extract patterns - handle both single model and model registry formats
1414
- spinner.setText('Importing patterns...');
1415
- const content = importData.pinataContent || importData;
1416
- let patterns = [];
1417
- // Check if this is a model registry (has models array)
1418
- const registry = content;
1419
- if (registry.models && Array.isArray(registry.models)) {
1420
- // Model registry format - extract patterns from each model
1421
- for (const model of registry.models) {
1422
- if (!categoryFilter || model.category === categoryFilter || model.id.includes(categoryFilter)) {
1423
- for (const pattern of model.patterns || []) {
1424
- patterns.push({
1425
- ...pattern,
1426
- category: model.category, // Tag with model category
1427
- });
1428
- }
1429
- }
1430
- }
1431
- }
1432
- else {
1433
- // Single model format - patterns at top level
1434
- patterns = content.patterns || [];
1435
- }
1436
- // Filter by category if specified (additional filtering)
1437
- if (categoryFilter && patterns.length > 0) {
1438
- patterns = patterns.filter(p => p.category === categoryFilter ||
1439
- p.trigger.includes(categoryFilter));
1440
- }
1441
- // Validate patterns (security check)
1442
- const validPatterns = patterns.filter(p => {
1443
- // Security: Reject patterns with suspicious content
1444
- const suspicious = [
1445
- 'eval(', 'Function(', 'exec(', 'spawn(',
1446
- 'child_process', 'rm -rf', 'sudo',
1447
- '<script>', 'javascript:', 'data:',
1448
- ];
1449
- const content = JSON.stringify(p);
1450
- return !suspicious.some(s => content.includes(s));
1451
- });
1452
- if (validPatterns.length < patterns.length) {
1453
- output.writeln(output.warning(`Filtered ${patterns.length - validPatterns.length} suspicious patterns`));
1454
- }
1455
- // Save to local memory
1456
- const memoryDir = path.join(process.cwd(), '.claude-flow', 'memory');
1457
- if (!fs.existsSync(memoryDir)) {
1458
- fs.mkdirSync(memoryDir, { recursive: true });
1459
- }
1460
- const patternsFile = path.join(memoryDir, 'patterns.json');
1461
- let existingPatterns = [];
1462
- if (merge && fs.existsSync(patternsFile)) {
1463
- existingPatterns = JSON.parse(fs.readFileSync(patternsFile, 'utf8'));
1464
- }
1465
- // Merge or replace
1466
- const existingIds = new Set(existingPatterns.map(p => p.id));
1467
- const newPatterns = validPatterns.filter(p => !existingIds.has(p.id));
1468
- const finalPatterns = merge ? [...existingPatterns, ...newPatterns] : validPatterns;
1469
- fs.writeFileSync(patternsFile, JSON.stringify(finalPatterns, null, 2));
1470
- spinner.succeed('Import complete');
1471
- output.writeln();
1472
- output.table({
1473
- columns: [
1474
- { key: 'metric', header: 'Metric', width: 25 },
1475
- { key: 'value', header: 'Value', width: 20 },
1476
- ],
1477
- data: [
1478
- { metric: 'Patterns Imported', value: String(validPatterns.length) },
1479
- { metric: 'New Patterns', value: String(newPatterns.length) },
1480
- { metric: 'Total Patterns', value: String(finalPatterns.length) },
1481
- { metric: 'Signature Verified', value: importData.signature ? 'Yes' : 'N/A' },
1482
- { metric: 'Merge Mode', value: merge ? 'Yes' : 'Replace' },
1483
- ],
1484
- });
1485
- output.writeln();
1486
- output.writeln(output.success('Patterns imported and ready to use'));
1487
- output.writeln(output.dim('Run "claude-flow neural patterns --action list" to see imported patterns'));
1488
- return { success: true };
1489
- }
1490
- catch (error) {
1491
- spinner.fail(`Import failed: ${error instanceof Error ? error.message : String(error)}`);
1492
- return { success: false, exitCode: 1 };
1493
- }
1494
- },
1495
- };
1496
- // Benchmark subcommand - Real WASM benchmarks
1497
- const benchmarkCommand = {
1498
- name: 'benchmark',
1499
- description: 'Benchmark RuVector WASM training performance',
1500
- options: [
1501
- { name: 'dim', short: 'd', type: 'number', description: 'Embedding dimension (max 256)', default: '256' },
1502
- { name: 'iterations', short: 'i', type: 'number', description: 'Number of iterations', default: '1000' },
1503
- { name: 'keys', short: 'k', type: 'number', description: 'Number of keys for attention', default: '100' },
1504
- ],
1505
- examples: [
1506
- { command: 'claude-flow neural benchmark', description: 'Run default benchmark' },
1507
- { command: 'claude-flow neural benchmark -d 128 -i 5000', description: 'Custom benchmark' },
1508
- ],
1509
- action: async (ctx) => {
1510
- const dim = Math.min(parseInt(ctx.flags.dim || '256', 10), 256);
1511
- const iterations = parseInt(ctx.flags.iterations || '1000', 10);
1512
- const numKeys = parseInt(ctx.flags.keys || '100', 10);
1513
- output.writeln();
1514
- output.writeln(output.bold('RuVector WASM Benchmark'));
1515
- output.writeln(output.dim('─'.repeat(50)));
1516
- const spinner = output.createSpinner({ text: 'Running benchmarks...', spinner: 'dots' });
1517
- spinner.start();
1518
- try {
1519
- // Indirect the specifier through a string variable so tsc doesn't
1520
- // statically resolve this optional dependency at build time (TS2307
1521
- // when it isn't installed — install-safety / Build V3 pattern from #2586).
1522
- const attentionPkg = '@ruvector/attention';
1523
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import of optional native WASM module with no type declarations
1524
- const attention = await import(attentionPkg);
1525
- // Manual benchmark since benchmarkAttention has a binding bug
1526
- const benchmarkMechanism = async (name, mechanism) => {
1527
- const query = new Float32Array(dim);
1528
- const keys = [];
1529
- const values = [];
1530
- for (let i = 0; i < dim; i++)
1531
- query[i] = Math.random();
1532
- for (let k = 0; k < numKeys; k++) {
1533
- const key = new Float32Array(dim);
1534
- const val = new Float32Array(dim);
1535
- for (let i = 0; i < dim; i++) {
1536
- key[i] = Math.random();
1537
- val[i] = Math.random();
1538
- }
1539
- keys.push(key);
1540
- values.push(val);
1541
- }
1542
- // Warmup
1543
- for (let i = 0; i < 10; i++)
1544
- mechanism.computeRaw(query, keys, values);
1545
- const start = performance.now();
1546
- for (let i = 0; i < iterations; i++) {
1547
- mechanism.computeRaw(query, keys, values);
1548
- }
1549
- const elapsed = performance.now() - start;
1550
- return {
1551
- name,
1552
- averageTimeMs: elapsed / iterations,
1553
- opsPerSecond: Math.round((iterations / elapsed) * 1000),
1554
- };
1555
- };
1556
- spinner.setText(`Benchmarking attention mechanisms (dim=${dim}, keys=${numKeys}, iter=${iterations})...`);
1557
- const results = [];
1558
- // Benchmark each mechanism
1559
- const dotProduct = new attention.DotProductAttention(dim);
1560
- results.push(await benchmarkMechanism('DotProduct', dotProduct));
1561
- const flash = new attention.FlashAttention(dim, 64);
1562
- results.push(await benchmarkMechanism('FlashAttention', flash));
1563
- const multiHead = new attention.MultiHeadAttention(dim, 4);
1564
- results.push(await benchmarkMechanism('MultiHead (4 heads)', multiHead));
1565
- const hyperbolic = new attention.HyperbolicAttention(dim, 1.0);
1566
- results.push(await benchmarkMechanism('Hyperbolic', hyperbolic));
1567
- const linear = new attention.LinearAttention(dim, dim);
1568
- results.push(await benchmarkMechanism('Linear', linear));
1569
- spinner.succeed('Benchmark complete');
1570
- output.writeln();
1571
- output.printTable({
1572
- columns: [
1573
- { key: 'name', header: 'Mechanism', width: 25 },
1574
- { key: 'avgTime', header: 'Avg Time (ms)', width: 15 },
1575
- { key: 'opsPerSec', header: 'Ops/sec', width: 15 },
1576
- ],
1577
- data: results.map(r => ({
1578
- name: r.name,
1579
- avgTime: r.averageTimeMs.toFixed(4),
1580
- opsPerSec: r.opsPerSecond.toLocaleString(),
1581
- })),
1582
- });
1583
- // Show speedup comparisons
1584
- const dotProductResult = results.find(r => r.name.includes('DotProduct'));
1585
- const flashResult = results.find(r => r.name.includes('Flash'));
1586
- const hyperbolicResult = results.find(r => r.name.includes('Hyperbolic'));
1587
- if (dotProductResult && flashResult) {
1588
- const speedup = dotProductResult.averageTimeMs / flashResult.averageTimeMs;
1589
- output.writeln();
1590
- output.writeln(output.highlight(`Flash Attention speedup: ${speedup.toFixed(2)}x faster than DotProduct`));
1591
- }
1592
- if (dotProductResult && hyperbolicResult) {
1593
- output.writeln(output.dim(`Hyperbolic overhead: ${(hyperbolicResult.averageTimeMs / dotProductResult.averageTimeMs).toFixed(2)}x (expected for manifold ops)`));
1594
- }
1595
- // Also benchmark MicroLoRA
1596
- spinner.start();
1597
- spinner.setText('Benchmarking MicroLoRA adaptation...');
1598
- // Load WASM file directly (Node.js compatible). Indirect the specifier
1599
- // through a string variable so tsc doesn't statically resolve this
1600
- // optional dependency at build time (TS2307 when absent — #2586 pattern).
1601
- const fs = await import('fs');
1602
- const { createRequire } = await import('module');
1603
- const require = createRequire(import.meta.url);
1604
- const learningWasmPkg = '@ruvector/learning-wasm';
1605
- const wasmPath = require.resolve(`${learningWasmPkg}/ruvector_learning_wasm_bg.wasm`);
1606
- const wasmBuffer = fs.readFileSync(wasmPath);
1607
- const learningWasm = await import(learningWasmPkg);
1608
- learningWasm.initSync({ module: wasmBuffer });
1609
- const lora = new learningWasm.WasmMicroLoRA(dim, 0.1, 0.01);
1610
- const gradient = new Float32Array(dim);
1611
- for (let i = 0; i < dim; i++)
1612
- gradient[i] = Math.random() - 0.5;
1613
- const loraStart = performance.now();
1614
- for (let i = 0; i < iterations; i++) {
1615
- lora.adapt_array(gradient);
1616
- }
1617
- const loraTime = performance.now() - loraStart;
1618
- const loraAvg = loraTime / iterations;
1619
- spinner.succeed('MicroLoRA benchmark complete');
1620
- output.writeln();
1621
- output.printTable({
1622
- columns: [
1623
- { key: 'metric', header: 'MicroLoRA Metric', width: 25 },
1624
- { key: 'value', header: 'Value', width: 25 },
1625
- ],
1626
- data: [
1627
- { metric: 'Dimension', value: String(dim) },
1628
- { metric: 'Iterations', value: iterations.toLocaleString() },
1629
- { metric: 'Total Time', value: `${loraTime.toFixed(2)}ms` },
1630
- { metric: 'Avg Adaptation', value: `${(loraAvg * 1000).toFixed(2)}μs` },
1631
- { metric: 'Adaptations/sec', value: Math.round(1000 / loraAvg).toLocaleString() },
1632
- { metric: 'Target (<100μs)', value: loraAvg * 1000 < 100 ? output.success('✓ PASS') : output.warning('✗ FAIL') },
1633
- ],
1634
- });
1635
- lora.free();
1636
- return { success: true, data: { results, loraAvg } };
1637
- }
1638
- catch (error) {
1639
- spinner.fail('Benchmark failed');
1640
- output.printError(error instanceof Error ? error.message : String(error));
1641
- return { success: false, exitCode: 1 };
1642
- }
1643
- },
1644
- };
1645
- // ============================================================================
1646
- // ADR-148 — `neural router` subcommand tree: status / train / reload
1647
- // ============================================================================
1648
- const routerStatusCommand = {
1649
- name: 'status',
1650
- description: 'Show the cost-optimal neural router state (ADR-148) — gate, backend, artifact, counters',
1651
- options: [
1652
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
1653
- ],
1654
- examples: [
1655
- { command: 'claude-flow neural router status', description: 'Show router state' },
1656
- { command: 'CLAUDE_FLOW_ROUTER_NEURAL=1 claude-flow neural router status', description: 'Show status with gate open' },
1657
- ],
1658
- action: async (ctx) => {
1659
- const format = ctx.flags.format || 'table';
1660
- const { neuralRouterStatus } = await import('../ruvector/neural-router.js');
1661
- const { getModelRouterStats } = await import('../ruvector/model-router.js');
1662
- const status = await neuralRouterStatus();
1663
- const stats = getModelRouterStats();
1664
- const payload = { neuralRouter: status, modelRouter: stats };
1665
- if (format === 'json') {
1666
- output.writeln(JSON.stringify(payload, null, 2));
1667
- return { success: true, data: payload };
1668
- }
1669
- output.writeln();
1670
- output.writeln(output.bold('Cost-Optimal Neural Router (ADR-148)'));
1671
- output.writeln(output.dim('─'.repeat(60)));
1672
- output.writeln(` Gate (CLAUDE_FLOW_ROUTER_NEURAL=1): ${status.enabled ? output.success('open') : output.warning('closed')}`);
1673
- output.writeln(` Backend available: ${status.available ? output.success('yes') : output.warning('no')}`);
1674
- output.writeln(` Active backend (routedBy): ${status.routedBy ?? '—'}`);
1675
- output.writeln(` Reason: ${status.reason}`);
1676
- output.writeln(` Quality bar: ${status.config.qualityBar}`);
1677
- output.writeln(` Seed corpus path: ${status.config.seedCorpusPath}`);
1678
- output.writeln(` Bundled KRR artifact path: ${status.config.bundledKrrPath}`);
1679
- output.writeln(` User artifact (modelPath): ${status.config.modelPath ?? '—'}`);
1680
- output.writeln();
1681
- output.writeln(output.bold('Counters (process-local since last reset)'));
1682
- output.writeln(output.dim('─'.repeat(60)));
1683
- output.writeln(` Total decisions: ${stats.totalDecisions}`);
1684
- output.writeln(` routedBy: heuristic=${stats.routedByCounts.heuristic} hybrid=${stats.routedByCounts.hybrid} bandit-fallback=${stats.routedByCounts['bandit-fallback']}`);
1685
- output.writeln(` neuralBackend: knn=${stats.neuralBackendCounts['metaharness-knn']} krr=${stats.neuralBackendCounts['metaharness-krr']} fastgrnn=${stats.neuralBackendCounts.fastgrnn}`);
1686
- output.writeln(` A/B mode: ${stats.ab.comparisons} comparisons, ${stats.ab.disagreements} disagreements (${(stats.ab.disagreementRate * 100).toFixed(1)}%)`);
1687
- output.writeln();
1688
- return { success: true, data: payload };
1689
- },
1690
- };
1691
- const routerTrainCommand = {
1692
- name: 'train',
1693
- description: 'Train a KRR router artifact from a DRACO-shaped JSON corpus (or the bundled seed) — pure TS, no native deps',
1694
- options: [
1695
- { name: 'corpus', short: 'c', type: 'string', description: 'Path to DRACO rows JSON ({embedding, scores}). Defaults to the bundled seed corpus.' },
1696
- { name: 'out', short: 'o', type: 'string', description: 'Output path for the trained KRR JSON.' },
1697
- { name: 'quality-bar', short: 'q', type: 'number', description: 'qualityBar for cost-optimal selection (default 0.8)', default: '0.8' },
1698
- ],
1699
- examples: [
1700
- { command: 'claude-flow neural router train', description: 'Train from the bundled seed and write to ./router.krr.json' },
1701
- { command: 'claude-flow neural router train -c ./my-corpus.json -o ./my-router.krr.json', description: 'Train from a custom corpus' },
1702
- ],
1703
- action: async (ctx) => {
1704
- const corpusPath = ctx.flags.corpus;
1705
- const outPath = ctx.flags.out || './router.krr.json';
1706
- const qualityBar = parseFloat(ctx.flags['quality-bar'] || '0.8') || 0.8;
1707
- // Indirect the optional-dep specifier through a string variable so tsc
1708
- // doesn't statically resolve `@metaharness/router` at build time (TS2307
1709
- // when it isn't installed — #2586 pattern). The dep is optional at
1710
- // runtime; the catch below emits a clear operator message.
1711
- const metaharnessRouterPkg = '@metaharness/router';
1712
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import of optional dep; call surface is fluid across upstream versions
1713
- let mh;
1714
- try {
1715
- mh = await import(metaharnessRouterPkg);
1716
- }
1717
- catch {
1718
- output.printError('@metaharness/router is not installed. `npm install @metaharness/router@^0.3.2` then re-run.');
1719
- return { success: false, exitCode: 1 };
1720
- }
1721
- const { neuralRouterStatus } = await import('../ruvector/neural-router.js');
1722
- const status = await neuralRouterStatus();
1723
- const fs = await import('node:fs');
1724
- const seedPath = corpusPath ?? status.config.seedCorpusPath;
1725
- if (!fs.existsSync(seedPath)) {
1726
- output.printError(`Corpus not found at ${seedPath}`);
1727
- return { success: false, exitCode: 1 };
1728
- }
1729
- const rows = JSON.parse(fs.readFileSync(seedPath, 'utf8'));
1730
- output.writeln();
1731
- output.writeln(output.bold('Training KRR router (ADR-148)'));
1732
- output.writeln(output.dim('─'.repeat(60)));
1733
- output.writeln(` Corpus: ${seedPath} (${rows.length} rows, dim=${rows[0]?.embedding?.length ?? '?'})`);
1734
- output.writeln(` Output: ${outPath}`);
1735
- output.writeln(` qualityBar: ${qualityBar}`);
1736
- output.writeln();
1737
- const spinner = output.createSpinner({ text: 'Fitting Beta-Bernoulli KRR with leave-one-out CV…', spinner: 'dots' });
1738
- spinner.start();
1739
- const t0 = performance.now();
1740
- const { router, lambda, looQuality } = mh.trainRouter(rows, { haiku: 1, sonnet: 3, opus: 15 }, {
1741
- qualityBar,
1742
- lambdas: [1e-4, 1e-3, 1e-2, 1e-1, 1e0],
1743
- });
1744
- const ms = performance.now() - t0;
1745
- spinner.succeed(`Trained in ${ms.toFixed(0)}ms (λ=${lambda.toExponential(3)}, looQuality=${looQuality.toFixed(4)})`);
1746
- fs.writeFileSync(outPath, JSON.stringify(router.toJSON()));
1747
- const bytes = fs.statSync(outPath).size;
1748
- output.writeln(`Wrote ${bytes} bytes → ${outPath}`);
1749
- output.writeln();
1750
- output.writeln(output.dim('Use: CLAUDE_FLOW_ROUTER_NEURAL=1 CLAUDE_FLOW_ROUTER_MODEL_PATH=' + outPath + ' …'));
1751
- output.writeln();
1752
- return { success: true, data: { lambda, looQuality, trainMs: ms, modelPath: outPath, modelBytes: bytes } };
1753
- },
1754
- };
1755
- // ADR-149 iter 19 — CLI surface for the iter 18 trajectory consumer. Pairs
1756
- // decision+outcome rows from the JSONL recorder into seed-rows-compatible
1757
- // training rows that `neural router train` (above) can consume.
1758
- const routerTrainFromTrajectoriesCommand = {
1759
- name: 'train-from-trajectories',
1760
- description: 'Pair production decision+outcome JSONL rows into seed-corpus-shaped training rows (ADR-149 iter 18/19)',
1761
- options: [
1762
- { name: 'in', short: 'i', type: 'string', description: 'Trajectory JSONL path (default: $CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH or .swarm/model-router-trajectories.jsonl)' },
1763
- { name: 'write', short: 'w', type: 'string', description: 'Write paired rows to this path (seed-rows.json-compatible JSON array)' },
1764
- { name: 'union', short: 'u', type: 'string', description: 'Union paired rows with an existing seed-rows.json — production rows win on task-text collision' },
1765
- { name: 'filter-source', type: 'string', description: 'Only keep outcomes whose source matches (e.g. llm-judge, agent-execute)' },
1766
- { name: 'min-quality', type: 'number', description: 'Drop pairs whose MAX outcome score is below this threshold (default 0 — keep failures, they are training signal)', default: '0' },
1767
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
1768
- ],
1769
- examples: [
1770
- { command: 'claude-flow neural router train-from-trajectories', description: 'Show pairing stats from default trajectory path' },
1771
- { command: 'claude-flow neural router train-from-trajectories -w production-rows.json', description: 'Emit a corpus to feed `router train`' },
1772
- { command: 'claude-flow neural router train-from-trajectories -u assets/model-router/seed-rows.json -w merged.json', description: 'Union production rows with the bundled seed corpus' },
1773
- { command: 'claude-flow neural router train-from-trajectories --filter-source llm-judge -w high-signal.json', description: 'Keep only judge-graded rows (drop coarse agent-execute baseline)' },
1774
- ],
1775
- action: async (ctx) => {
1776
- const fs = await import('node:fs');
1777
- const path = await import('node:path');
1778
- const { pairTrajectoryRows } = await import('../ruvector/router-trajectory.js');
1779
- const inPath = ctx.flags.in
1780
- ?? process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH
1781
- ?? path.resolve(process.cwd(), '.swarm', 'model-router-trajectories.jsonl');
1782
- const writePath = ctx.flags.write;
1783
- const unionPath = ctx.flags.union;
1784
- // Argv parser may camelCase hyphenated flags — accept both spellings.
1785
- const filterSource = (ctx.flags['filter-source'] ?? ctx.flags.filterSource);
1786
- const minQuality = parseFloat((ctx.flags['min-quality'] ?? ctx.flags.minQuality) || '0') || 0;
1787
- const fmt = ctx.flags.format || 'table';
1788
- if (!fs.existsSync(inPath)) {
1789
- const msg = `Trajectory file not found at ${inPath}`;
1790
- if (fmt === 'json') {
1791
- output.writeln(JSON.stringify({ error: msg, hint: 'Set CLAUDE_FLOW_ROUTER_TRAJECTORY=1 and run any agent_spawn → executeAgentTask flow to accumulate rows.' }, null, 2));
1792
- }
1793
- else {
1794
- output.printError(msg);
1795
- output.writeln(output.dim(' Set CLAUDE_FLOW_ROUTER_TRAJECTORY=1 to enable trajectory recording.'));
1796
- }
1797
- return { success: false, exitCode: 1 };
1798
- }
1799
- const text = fs.readFileSync(inPath, 'utf8');
1800
- const lines = text.split('\n').filter(l => l.trim().length > 0);
1801
- const allRows = [];
1802
- let malformed = 0;
1803
- for (const l of lines) {
1804
- try {
1805
- allRows.push(JSON.parse(l));
1806
- }
1807
- catch {
1808
- malformed++;
1809
- }
1810
- }
1811
- const { pairs: rawPairs, stats } = pairTrajectoryRows(allRows);
1812
- let pairs = rawPairs;
1813
- let filteredCount = pairs.length;
1814
- if (filterSource)
1815
- pairs = pairs.filter(p => p.source === filterSource);
1816
- if (minQuality > 0)
1817
- pairs = pairs.filter(p => Math.max(...Object.values(p.scores)) >= minQuality);
1818
- filteredCount = pairs.length;
1819
- const corpusRows = pairs.map(p => ({
1820
- task: p.task,
1821
- embedding: p.embedding,
1822
- scores: p.scores,
1823
- tier: p.tier,
1824
- }));
1825
- let unionRows = corpusRows;
1826
- let unioned = false;
1827
- let seedKept = 0;
1828
- if (unionPath) {
1829
- if (!fs.existsSync(unionPath)) {
1830
- output.printError(`--union path ${unionPath} not found`);
1831
- return { success: false, exitCode: 1 };
1832
- }
1833
- const seedRows = JSON.parse(fs.readFileSync(unionPath, 'utf8'));
1834
- const productionTasks = new Set(corpusRows.map(r => r.task));
1835
- const kept = seedRows.filter(r => !productionTasks.has(r.task));
1836
- seedKept = kept.length;
1837
- unionRows = [...kept, ...corpusRows];
1838
- unioned = true;
1839
- }
1840
- if (writePath) {
1841
- fs.writeFileSync(writePath, JSON.stringify(unionRows));
1842
- }
1843
- const data = {
1844
- input: inPath,
1845
- malformed,
1846
- stats,
1847
- afterFilters: filteredCount,
1848
- unioned,
1849
- seedKept: unioned ? seedKept : undefined,
1850
- finalRows: unionRows.length,
1851
- written: writePath,
1852
- };
1853
- if (fmt === 'json') {
1854
- output.writeln(JSON.stringify(data, null, 2));
1855
- return { success: true, data };
1856
- }
1857
- output.writeln();
1858
- output.writeln(output.bold('Trajectory → Training-row pairing (ADR-149 iter 18/19)'));
1859
- output.writeln(output.dim('─'.repeat(60)));
1860
- output.writeln(` Input file: ${inPath}`);
1861
- output.writeln(` Total rows: ${stats.totalRows} (${stats.decisions} decision, ${stats.outcomes} outcome${malformed > 0 ? `, ${malformed} malformed` : ''})`);
1862
- output.writeln(` Paired: ${stats.paired}`);
1863
- output.writeln(` Dropped (no embed): ${stats.droppedNoEmbedding}`);
1864
- output.writeln(` Dropped (no match): ${stats.droppedNoMatch}`);
1865
- if (Object.keys(stats.bySource).length > 0)
1866
- output.writeln(` By source: ${JSON.stringify(stats.bySource)}`);
1867
- if (Object.keys(stats.byTier).length > 0)
1868
- output.writeln(` By tier: ${JSON.stringify(stats.byTier)}`);
1869
- output.writeln(` After filters: ${filteredCount}`);
1870
- if (unioned)
1871
- output.writeln(` Final (unioned): ${unionRows.length} (seed kept=${seedKept}, production=${corpusRows.length})`);
1872
- if (writePath)
1873
- output.writeln(` Written: ${writePath}`);
1874
- output.writeln();
1875
- if (stats.totalRows === 0) {
1876
- output.writeln(output.dim(' Empty trajectory file. Enable recording with `export CLAUDE_FLOW_ROUTER_TRAJECTORY=1` and run agent_spawn flows.'));
1877
- }
1878
- else if (pairs.length > 0 && writePath) {
1879
- output.writeln(output.dim(` Next: claude-flow neural router train -c ${writePath} -o router.krr.json`));
1880
- }
1881
- output.writeln();
1882
- return { success: true, data };
1883
- },
1884
- };
1885
- const routerReloadCommand = {
1886
- name: 'reload',
1887
- description: 'Force-reload the neural router (clears in-process backend cache; next call re-reads artifact/corpus)',
1888
- options: [],
1889
- examples: [
1890
- { command: 'claude-flow neural router reload', description: 'Refresh backend caches after retraining an artifact' },
1891
- ],
1892
- action: async () => {
1893
- const { __resetNeuralRouterForTests, neuralRouterStatus } = await import('../ruvector/neural-router.js');
1894
- __resetNeuralRouterForTests();
1895
- output.writeln(output.success('Neural router backend cache cleared.'));
1896
- const status = await neuralRouterStatus();
1897
- output.writeln(output.dim(` Active backend now: ${status.routedBy ?? '—'} (${status.reason})`));
1898
- return { success: true, data: status };
1899
- },
1900
- };
1901
- /**
1902
- * ADR-149 iter 8 — `neural router models`: list the candidate registry
1903
- * with measured per-tier scores from the most recent seed-corpus bench,
1904
- * latency, and cost. Reads:
1905
- * - assets/model-router/seed-rows.json (which model ids exist in the corpus)
1906
- * - latest docs/benchmarks/runs/seed-corpus-*.json (per-candidate aggregates)
1907
- * - assets/model-router/openrouter-alts.json (tier mapping + alt rankings)
1908
- * Falls back gracefully when files are missing.
1909
- */
1910
- const routerModelsCommand = {
1911
- name: 'models',
1912
- description: 'List the cost-optimal router registry with measured per-tier scores, latency, and cost (ADR-149)',
1913
- options: [
1914
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
1915
- ],
1916
- examples: [
1917
- { command: 'claude-flow neural router models', description: 'Show candidate registry with measured stats' },
1918
- { command: 'claude-flow neural router models -f json', description: 'Machine-readable JSON' },
1919
- ],
1920
- action: async (ctx) => {
1921
- const fmt = ctx.flags.format || 'table';
1922
- const fs = await import('node:fs');
1923
- const path = await import('node:path');
1924
- const { neuralRouterStatus } = await import('../ruvector/neural-router.js');
1925
- const status = await neuralRouterStatus();
1926
- const seedPath = status.config.seedCorpusPath;
1927
- // Best-effort: the candidate set is the union of ids appearing in
1928
- // seed-rows.json scores keys. The latest measurement run's
1929
- // perCandidate array is the authoritative source for measured stats.
1930
- const candidates = new Map();
1931
- try {
1932
- if (fs.existsSync(seedPath)) {
1933
- const rows = JSON.parse(fs.readFileSync(seedPath, 'utf8'));
1934
- for (const r of rows) {
1935
- for (const id of Object.keys(r.scores ?? {})) {
1936
- if (!candidates.has(id))
1937
- candidates.set(id, { id, tier: 'unknown' });
1938
- }
1939
- }
1940
- }
1941
- }
1942
- catch { /* keep going */ }
1943
- // Pull latest FULL-CORPUS measurement for per-tier scores + latency.
1944
- // Prefer files with cheap+mid+strong all populated (40-row+ runs); fall
1945
- // back to the most-recent partial file if no full run is found.
1946
- try {
1947
- const benchDir = path.resolve(process.cwd(), 'docs', 'benchmarks', 'runs');
1948
- if (fs.existsSync(benchDir)) {
1949
- const files = fs.readdirSync(benchDir)
1950
- .filter(f => f.startsWith('seed-corpus-') && f.endsWith('.json'))
1951
- .sort()
1952
- .reverse();
1953
- // Scan files newest-first; pick the first one that has all three
1954
- // tiers populated for the median model.
1955
- let chosen = null;
1956
- for (const f of files) {
1957
- try {
1958
- const data = JSON.parse(fs.readFileSync(path.join(benchDir, f), 'utf8'));
1959
- const sample = data.perCandidate?.[0];
1960
- if (sample && sample.cheap_avg_score != null && sample.mid_avg_score != null && sample.strong_avg_score != null) {
1961
- chosen = data;
1962
- break;
1963
- }
1964
- if (!chosen)
1965
- chosen = data; // fallback to newest
1966
- }
1967
- catch { /* skip malformed */ }
1968
- }
1969
- if (chosen) {
1970
- for (const raw of chosen.perCandidate ?? []) {
1971
- const r = raw;
1972
- const existing = candidates.get(r.id);
1973
- const c = existing ?? { id: r.id, tier: 'unknown' };
1974
- c.tier = (r.tier ?? c.tier) || 'unknown';
1975
- c.cost_in = r.cost_per_m_tok_in;
1976
- c.cost_out = r.cost_per_m_tok_out;
1977
- c.cheap = r.cheap_avg_score ?? undefined;
1978
- c.mid = r.mid_avg_score ?? undefined;
1979
- c.strong = r.strong_avg_score ?? undefined;
1980
- c.overall = r.overall_avg_score ?? undefined;
1981
- c.latency_ms = r.latency_mean_ms ?? undefined;
1982
- candidates.set(r.id, c);
1983
- }
1984
- }
1985
- }
1986
- }
1987
- catch { /* keep going */ }
1988
- const rows = Array.from(candidates.values()).sort((a, b) => (b.overall ?? -1) - (a.overall ?? -1));
1989
- if (fmt === 'json') {
1990
- output.writeln(JSON.stringify({ count: rows.length, candidates: rows }, null, 2));
1991
- return { success: true, data: { count: rows.length, candidates: rows } };
1992
- }
1993
- output.writeln();
1994
- output.writeln(output.bold('Cost-Optimal Router Registry (ADR-149)'));
1995
- output.writeln(output.dim('─'.repeat(60)));
1996
- output.writeln(` Source: ${seedPath}`);
1997
- output.writeln(` Candidates: ${rows.length}`);
1998
- output.writeln();
1999
- if (rows.length === 0) {
2000
- output.writeln(output.warning(' No candidates — corpus not generated or measurement not run.'));
2001
- output.writeln(output.dim(' Run: node scripts/gen-seed-corpus-v2.mjs && OPENROUTER_API_KEY=... node scripts/benchmark-seed-corpus.mjs --live'));
2002
- return { success: true, data: { count: 0 } };
2003
- }
2004
- const fmtPct = (v) => v == null ? '—'.padStart(6) : `${(v * 100).toFixed(1)}%`.padStart(6);
2005
- const fmtCost = (v) => v == null ? '—' : `$${v.toFixed(2)}`;
2006
- const fmtLat = (v) => v == null ? '—' : `${v.toFixed(0)}ms`;
2007
- output.writeln(' | Candidate | Tier | Cheap | Mid | Strong | Overall | $/Mtok in/out | Latency |');
2008
- output.writeln(' |--------------------------------------------|--------|--------|--------|--------|---------|---------------|---------|');
2009
- for (const c of rows) {
2010
- output.writeln(` | ${c.id.padEnd(42)} | ${c.tier.padEnd(6)} | ${fmtPct(c.cheap)} | ${fmtPct(c.mid)} | ${fmtPct(c.strong)} | ${fmtPct(c.overall)} | ${fmtCost(c.cost_in).padStart(5)}/${fmtCost(c.cost_out).padEnd(6)} | ${fmtLat(c.latency_ms).padStart(7)} |`);
2011
- }
2012
- output.writeln();
2013
- output.writeln(output.dim(' Sorted by overall score desc. Empty cells = no measurement on that tier.'));
2014
- output.writeln(output.dim(' To re-measure: OPENROUTER_API_KEY=... node scripts/benchmark-seed-corpus.mjs --live'));
2015
- output.writeln();
2016
- return { success: true, data: { count: rows.length, candidates: rows } };
2017
- },
2018
- };
2019
- // ADR-149 iter 28 — observability into recorded routing decisions. The
2020
- // trajectory JSONL (iter 17+) records every decision the router makes;
2021
- // this subcommand exposes that data as filtered + aggregated views so
2022
- // operators don't have to grep the file.
2023
- const routerDecisionsCommand = {
2024
- name: 'decisions',
2025
- description: 'Query the routing-decision JSONL (iter 17+): filter, aggregate, paginate (ADR-149 iter 28)',
2026
- options: [
2027
- { name: 'in', short: 'i', type: 'string', description: 'Trajectory JSONL path (default: $CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH or .swarm/model-router-trajectories.jsonl)' },
2028
- { name: 'since', short: 's', type: 'string', description: 'Time window suffix: 1h, 24h, 7d, 30d (default: all)' },
2029
- { name: 'routed-by', type: 'string', description: 'Filter by decision mechanism: hybrid | bandit-fallback | heuristic' },
2030
- { name: 'model', short: 'm', type: 'string', description: 'Filter by chosen model id (substring match, e.g. haiku, gpt-4)' },
2031
- { name: 'bucket', type: 'string', description: 'Filter by complexity bucket: cheap (< 0.34) | mid (< 0.67) | strong (≥ 0.67) — iter 58' },
2032
- { name: 'task-hash', type: 'string', description: 'Filter by exact task_hash (FNV-1a-32). Use for incident investigation — iter 59' },
2033
- { name: 'limit', short: 'l', type: 'number', description: 'Max recent decisions to list (default 20)', default: '20' },
2034
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
2035
- ],
2036
- examples: [
2037
- { command: 'claude-flow neural router decisions', description: 'Aggregate stats + 20 most-recent decisions' },
2038
- { command: 'claude-flow neural router decisions --since 24h', description: 'Last 24 hours only' },
2039
- { command: 'claude-flow neural router decisions --routed-by bandit-fallback', description: 'Find decisions where neural backend failed' },
2040
- { command: 'claude-flow neural router decisions --model haiku --format json', description: 'All haiku picks, JSON output' },
2041
- ],
2042
- action: async (ctx) => {
2043
- const fs = await import('node:fs');
2044
- const path = await import('node:path');
2045
- const inPath = ctx.flags.in
2046
- ?? process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH
2047
- ?? path.resolve(process.cwd(), '.swarm', 'model-router-trajectories.jsonl');
2048
- const since = ctx.flags.since;
2049
- const routedByFilter = (ctx.flags['routed-by'] ?? ctx.flags.routedBy);
2050
- const modelFilter = ctx.flags.model;
2051
- const bucketFilterRaw = ctx.flags.bucket?.toLowerCase();
2052
- const bucketFilter = bucketFilterRaw === 'cheap' || bucketFilterRaw === 'mid' || bucketFilterRaw === 'strong'
2053
- ? bucketFilterRaw : undefined;
2054
- if (bucketFilterRaw && !bucketFilter) {
2055
- output.printError(`--bucket must be one of: cheap | mid | strong (got "${bucketFilterRaw}")`);
2056
- return { success: false, exitCode: 1 };
2057
- }
2058
- // iter 59 — task_hash filter for incident investigation. Accepts the 8-char
2059
- // FNV-1a-32 hex format the trajectory recorder uses.
2060
- const taskHashFilter = (ctx.flags['task-hash'] ?? ctx.flags.taskHash)?.toLowerCase();
2061
- if (taskHashFilter && !/^[0-9a-f]{8}$/.test(taskHashFilter)) {
2062
- output.printError(`--task-hash must be an 8-char hex string (got "${taskHashFilter}")`);
2063
- return { success: false, exitCode: 1 };
2064
- }
2065
- const limit = parseInt(ctx.flags.limit || '20', 10) || 20;
2066
- const fmt = ctx.flags.format || 'table';
2067
- if (!fs.existsSync(inPath)) {
2068
- const msg = `Trajectory file not found at ${inPath}`;
2069
- if (fmt === 'json') {
2070
- output.writeln(JSON.stringify({ error: msg, hint: 'Set CLAUDE_FLOW_ROUTER_TRAJECTORY=1 to enable recording.' }, null, 2));
2071
- }
2072
- else {
2073
- output.printError(msg);
2074
- output.writeln(output.dim(' Set CLAUDE_FLOW_ROUTER_TRAJECTORY=1 to enable trajectory recording.'));
2075
- }
2076
- return { success: false, exitCode: 1 };
2077
- }
2078
- const text = fs.readFileSync(inPath, 'utf8');
2079
- const lines = text.split('\n').filter(l => l.trim().length > 0);
2080
- const decisions = [];
2081
- const outcomesByHash = new Map();
2082
- let malformed = 0;
2083
- for (const l of lines) {
2084
- try {
2085
- const row = JSON.parse(l);
2086
- if (row.type === 'decision') {
2087
- decisions.push(row);
2088
- }
2089
- else if (row.type === 'outcome') {
2090
- const arr = outcomesByHash.get(row.task_hash) ?? [];
2091
- arr.push({
2092
- task_hash: row.task_hash,
2093
- ts: row.ts,
2094
- quality: row.quality,
2095
- cost_usd: row.cost_usd,
2096
- tokens: row.tokens,
2097
- model_id: row.model_id,
2098
- });
2099
- outcomesByHash.set(row.task_hash, arr);
2100
- }
2101
- }
2102
- catch {
2103
- malformed++;
2104
- }
2105
- }
2106
- // Time-window filter.
2107
- let cutoffMs = null;
2108
- if (since) {
2109
- const m = since.match(/^(\d+)([hdmw])$/);
2110
- if (m) {
2111
- const n = parseInt(m[1], 10);
2112
- const unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000 }[m[2]] ?? 0;
2113
- cutoffMs = Date.now() - n * unitMs;
2114
- }
2115
- }
2116
- let filtered = decisions;
2117
- if (cutoffMs !== null) {
2118
- filtered = filtered.filter(d => Date.parse(d.ts) >= cutoffMs);
2119
- }
2120
- if (routedByFilter) {
2121
- filtered = filtered.filter(d => d.routed_by === routedByFilter);
2122
- }
2123
- if (bucketFilter) {
2124
- filtered = filtered.filter(d => {
2125
- const bucket = d.complexity < 0.34 ? 'cheap' : d.complexity < 0.67 ? 'mid' : 'strong';
2126
- return bucket === bucketFilter;
2127
- });
2128
- }
2129
- if (taskHashFilter) {
2130
- filtered = filtered.filter(d => d.task_hash === taskHashFilter);
2131
- }
2132
- if (modelFilter) {
2133
- const needle = modelFilter.toLowerCase();
2134
- filtered = filtered.filter(d => {
2135
- const id = (d.openrouter_model ?? d.model).toLowerCase();
2136
- return id.includes(needle);
2137
- });
2138
- }
2139
- // Aggregate.
2140
- const byRoutedBy = {};
2141
- const byModel = {};
2142
- const byTier = { cheap: 0, mid: 0, strong: 0 };
2143
- // iter 31 — cost aggregation. Sum cost_usd from paired outcome rows.
2144
- let costTotalUsd = 0;
2145
- let costPairedCount = 0;
2146
- const costByModel = {};
2147
- const costByTier = { cheap: 0, mid: 0, strong: 0 };
2148
- // iter 65 — track per-hash decision-iteration so we can pair the N-th
2149
- // decision for a hash with the N-th outcome (instead of always pulling
2150
- // the latest outcome). Iter 17's recorder writes decision then outcome
2151
- // in chronological order, so this index-pairing matches what the agent
2152
- // actually dispatched.
2153
- const decisionIndexByHash = new Map();
2154
- for (const d of filtered) {
2155
- byRoutedBy[d.routed_by] = (byRoutedBy[d.routed_by] ?? 0) + 1;
2156
- const id = d.openrouter_model ?? d.model;
2157
- byModel[id] = (byModel[id] ?? 0) + 1;
2158
- const tier = d.complexity < 0.34 ? 'cheap' : d.complexity < 0.67 ? 'mid' : 'strong';
2159
- byTier[tier]++;
2160
- // iter 31 + iter 65 — JOIN to the i-th OUTCOME row for this task_hash
2161
- // (where i is the i-th DECISION for this hash). Avoids double-counting
2162
- // costs when a task ran multiple times.
2163
- const outcomeArr = outcomesByHash.get(d.task_hash);
2164
- if (outcomeArr && outcomeArr.length > 0) {
2165
- const seen = decisionIndexByHash.get(d.task_hash) ?? 0;
2166
- const out = outcomeArr[Math.min(seen, outcomeArr.length - 1)];
2167
- decisionIndexByHash.set(d.task_hash, seen + 1);
2168
- if (out?.cost_usd != null) {
2169
- costTotalUsd += out.cost_usd;
2170
- costPairedCount++;
2171
- const modelKey = out.model_id ?? id;
2172
- costByModel[modelKey] = (costByModel[modelKey] ?? 0) + out.cost_usd;
2173
- costByTier[tier] += out.cost_usd;
2174
- }
2175
- }
2176
- }
2177
- const fallbackRate = filtered.length > 0
2178
- ? ((byRoutedBy['bandit-fallback'] ?? 0) / filtered.length) * 100
2179
- : 0;
2180
- // Sort by ts ascending so "most recent" is well-defined regardless of
2181
- // file order (rotation, concurrent writes can break monotonicity).
2182
- const sorted = [...filtered].sort((a, b) => a.ts.localeCompare(b.ts));
2183
- const recent = sorted.slice(-limit).reverse(); // newest first
2184
- const payload = {
2185
- input: inPath,
2186
- totalRows: lines.length,
2187
- decisionRows: decisions.length,
2188
- malformed,
2189
- filtered: filtered.length,
2190
- filters: { since, routedBy: routedByFilter, model: modelFilter, bucket: bucketFilter, taskHash: taskHashFilter },
2191
- aggregates: {
2192
- byRoutedBy, byModel, byTier,
2193
- fallbackRatePct: Math.round(fallbackRate * 100) / 100,
2194
- // iter 31 — cost aggregates. Only populated when outcome rows
2195
- // carry cost_usd (post-iter-31 trajectories).
2196
- ...(costPairedCount > 0 ? {
2197
- costTotalUsd: Math.round(costTotalUsd * 1000000) / 1000000,
2198
- costPairedCount,
2199
- costByModel: Object.fromEntries(Object.entries(costByModel).map(([k, v]) => [k, Math.round(v * 1000000) / 1000000])),
2200
- costByTier: Object.fromEntries(Object.entries(costByTier).map(([k, v]) => [k, Math.round(v * 1000000) / 1000000])),
2201
- avgCostPerCall: Math.round((costTotalUsd / costPairedCount) * 1000000) / 1000000,
2202
- } : {}),
2203
- },
2204
- recent,
2205
- };
2206
- if (fmt === 'json') {
2207
- output.writeln(JSON.stringify(payload, null, 2));
2208
- return { success: true, data: payload };
2209
- }
2210
- output.writeln();
2211
- output.writeln(output.bold('Routing decisions (ADR-149 iter 17+, query iter 28)'));
2212
- output.writeln(output.dim('─'.repeat(72)));
2213
- output.writeln(` Input: ${inPath}`);
2214
- output.writeln(` JSONL rows: ${lines.length} (${decisions.length} decision, ${malformed} malformed)`);
2215
- if (since || routedByFilter || modelFilter || bucketFilter || taskHashFilter) {
2216
- output.writeln(` Filters: ${[since && `since=${since}`, routedByFilter && `routed-by=${routedByFilter}`, modelFilter && `model~${modelFilter}`, bucketFilter && `bucket=${bucketFilter}`, taskHashFilter && `task_hash=${taskHashFilter}`].filter(Boolean).join(', ')}`);
2217
- }
2218
- output.writeln(` After filters: ${filtered.length}`);
2219
- output.writeln('');
2220
- if (filtered.length === 0) {
2221
- output.writeln(output.dim(' No decisions match the filters.'));
2222
- output.writeln('');
2223
- return { success: true, data: payload };
2224
- }
2225
- output.writeln(` Fallback rate: ${fallbackRate.toFixed(2)}% (neural backend → bandit when prediction unusable)`);
2226
- output.writeln('');
2227
- output.writeln(' By routed_by:');
2228
- for (const [k, v] of Object.entries(byRoutedBy).sort((a, b) => b[1] - a[1])) {
2229
- const pct = ((v / filtered.length) * 100).toFixed(1).padStart(5);
2230
- output.writeln(` ${k.padEnd(18)} ${String(v).padStart(6)} ${pct}%`);
2231
- }
2232
- output.writeln('');
2233
- output.writeln(' By model:');
2234
- for (const [k, v] of Object.entries(byModel).sort((a, b) => b[1] - a[1])) {
2235
- const pct = ((v / filtered.length) * 100).toFixed(1).padStart(5);
2236
- output.writeln(` ${k.padEnd(42)} ${String(v).padStart(6)} ${pct}%`);
2237
- }
2238
- output.writeln('');
2239
- output.writeln(' By tier (complexity bucket):');
2240
- for (const [k, v] of Object.entries(byTier)) {
2241
- const pct = filtered.length > 0 ? ((v / filtered.length) * 100).toFixed(1).padStart(5) : ' 0.0';
2242
- output.writeln(` ${k.padEnd(8)} ${String(v).padStart(6)} ${pct}%`);
2243
- }
2244
- output.writeln('');
2245
- // iter 31 — cost block (only when outcome rows carry cost_usd).
2246
- if (costPairedCount > 0) {
2247
- output.writeln(' Cost (USD, from paired outcomes):');
2248
- output.writeln(` Total: $${costTotalUsd.toFixed(4)} across ${costPairedCount} paired decisions`);
2249
- output.writeln(` Avg per call: $${(costTotalUsd / costPairedCount).toFixed(6)}`);
2250
- output.writeln(' By model:');
2251
- for (const [k, v] of Object.entries(costByModel).sort((a, b) => b[1] - a[1])) {
2252
- output.writeln(` ${k.padEnd(40)} $${v.toFixed(4)}`);
2253
- }
2254
- output.writeln(' By tier:');
2255
- for (const [k, v] of Object.entries(costByTier)) {
2256
- output.writeln(` ${k.padEnd(8)} $${v.toFixed(4)}`);
2257
- }
2258
- output.writeln('');
2259
- }
2260
- // iter 59 — incident detail mode. When --task-hash is set, render each
2261
- // matching decision with FULL task text, paired outcome, complexity,
2262
- // ab_pair, ensemble_disagreement. Operators investigating "why was
2263
- // THIS task routed to X?" want maximum context per decision.
2264
- if (taskHashFilter && filtered.length > 0) {
2265
- // iter 64 — sort occurrences newest-first by ts. JSONL insertion order
2266
- // may not match chronological order after rotation or out-of-order writes.
2267
- const incidentSorted = [...filtered].sort((a, b) => b.ts.localeCompare(a.ts));
2268
- output.writeln(output.bold(` Incident detail for task_hash=${taskHashFilter} (${filtered.length} occurrence(s), newest first):`));
2269
- for (const d of incidentSorted) {
2270
- // iter 65 — outcomesByHash is now an array of all outcomes per hash.
2271
- // For the incident detail at this decision's timestamp, pick the
2272
- // outcome closest in time (typically the corresponding one written
2273
- // by iter 17's recorder immediately after the decision).
2274
- const allOutcomes = outcomesByHash.get(d.task_hash) ?? [];
2275
- const dts = Date.parse(d.ts);
2276
- let out;
2277
- let bestDelta = Infinity;
2278
- for (const o of allOutcomes) {
2279
- const delta = Math.abs(Date.parse(o.ts) - dts);
2280
- if (delta < bestDelta) {
2281
- bestDelta = delta;
2282
- out = o;
2283
- }
2284
- }
2285
- const bucket = d.complexity < 0.34 ? 'cheap' : d.complexity < 0.67 ? 'mid' : 'strong';
2286
- output.writeln('');
2287
- output.writeln(` ts: ${d.ts}`);
2288
- output.writeln(` task: "${d.task ?? '<task text not stored>'}"`);
2289
- output.writeln(` complexity: ${d.complexity.toFixed(3)} (bucket: ${bucket})`);
2290
- output.writeln(` picked model: ${d.openrouter_model ?? d.model}`);
2291
- output.writeln(` routed_by: ${d.routed_by}${d.neural_backend ? ` via ${d.neural_backend}` : ''}`);
2292
- output.writeln(` confidence: ${d.confidence.toFixed(3)} uncertainty: ${d.uncertainty.toFixed(3)}`);
2293
- const apd = d.ab_pair;
2294
- if (apd) {
2295
- output.writeln(` ab_pair: bandit=${apd.bandit_pick} hybrid=${apd.hybrid_pick} disagree=${apd.disagree}`);
2296
- }
2297
- const ed = d.ensemble_disagreement;
2298
- if (typeof ed === 'number') {
2299
- output.writeln(` ensemble disagree: ${ed.toFixed(4)}`);
2300
- }
2301
- if (out) {
2302
- output.writeln(` outcome: quality=${out.quality ?? '—'} cost_usd=${out.cost_usd != null ? '$' + out.cost_usd.toFixed(6) : '—'} source=${out.source ?? '—'}`);
2303
- }
2304
- else {
2305
- output.writeln(` outcome: ${output.warning('(no paired outcome row)')}`);
2306
- }
2307
- }
2308
- output.writeln('');
2309
- return { success: true, data: payload };
2310
- }
2311
- output.writeln(` ${Math.min(limit, recent.length)} most-recent decisions (newest first):`);
2312
- output.writeln(' ' + 'ts'.padEnd(20) + 'routed_by'.padEnd(18) + 'model'.padEnd(34) + 'conf');
2313
- for (const d of recent) {
2314
- const ts = d.ts.slice(0, 19);
2315
- const id = (d.openrouter_model ?? d.model).slice(0, 32);
2316
- output.writeln(` ${ts.padEnd(20)}${d.routed_by.padEnd(18)}${id.padEnd(34)}${d.confidence.toFixed(2)}`);
2317
- }
2318
- output.writeln('');
2319
- return { success: true, data: payload };
2320
- },
2321
- };
2322
- // ADR-149 iter 30 — forward-direction observability. Pairs with iter 28's
2323
- // backward-direction `decisions` query. Lets operators inspect what the
2324
- // router would pick for a hypothetical task WITHOUT actually dispatching.
2325
- const routerDecideCommand = {
2326
- name: 'decide',
2327
- description: 'Show what the router would pick for a given task — no dispatch, just inspection (ADR-149 iter 30)',
2328
- options: [
2329
- { name: 'task', short: 't', type: 'string', description: 'Task text (alternatively pass as positional arg)' },
2330
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
2331
- ],
2332
- examples: [
2333
- { command: 'claude-flow neural router decide "fix typo in cache.ts"', description: 'See the routing decision for a small task' },
2334
- { command: 'claude-flow neural router decide -t "design distributed consensus" -f json', description: 'JSON output for the routing decision' },
2335
- { command: 'CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK=5 claude-flow neural router decide -t "..."', description: 'See iter 29 cost-ceiling mode in action' },
2336
- ],
2337
- action: async (ctx) => {
2338
- const task = ctx.flags.task ?? (ctx.args && ctx.args[0]) ?? null;
2339
- const fmt = ctx.flags.format || 'table';
2340
- if (!task || typeof task !== 'string' || task.trim().length === 0) {
2341
- output.printError('Provide a task: --task "..." or as a positional argument');
2342
- return { success: false, exitCode: 1 };
2343
- }
2344
- const { analyzeTaskComplexity, routeToModelFull } = await import('../ruvector/model-router.js');
2345
- const { embedTaskWithCache } = await import('../ruvector/task-embedder.js');
2346
- const { neuralRouterStatus, tryCostOptimalRoute } = await import('../ruvector/neural-router.js');
2347
- const t0 = performance.now();
2348
- const complexity = analyzeTaskComplexity(task);
2349
- let embedding;
2350
- try {
2351
- embedding = await embedTaskWithCache(task);
2352
- }
2353
- catch { /* embedder may be absent */ }
2354
- const result = await routeToModelFull(task, embedding);
2355
- const status = await neuralRouterStatus();
2356
- // iter 45 — also fetch the neural-layer result so we can surface the
2357
- // ensemble-disagreement diagnostic. Extra inference but `decide` is an
2358
- // operator inspection tool, not the hot path.
2359
- let neuralResult = null;
2360
- if (embedding) {
2361
- const bucket = complexity.score < 0.34 ? 'low' : complexity.score < 0.67 ? 'med' : 'high';
2362
- try {
2363
- neuralResult = await tryCostOptimalRoute(embedding, { complexityBucket: bucket });
2364
- }
2365
- catch { /* gated off or backend absent */ }
2366
- }
2367
- const ms = performance.now() - t0;
2368
- const payload = {
2369
- task: task.length > 200 ? task.slice(0, 200) + '…' : task,
2370
- taskLength: task.length,
2371
- hasEmbedding: !!embedding,
2372
- embeddingDim: embedding?.length ?? null,
2373
- complexity: {
2374
- score: complexity.score,
2375
- bucket: complexity.score < 0.34 ? 'low' : complexity.score < 0.67 ? 'med' : 'high',
2376
- features: complexity.features,
2377
- indicators: complexity.indicators,
2378
- },
2379
- decision: {
2380
- model: result.model,
2381
- modelId: result.modelId,
2382
- provider: result.provider,
2383
- openrouterModel: result.openrouterModel,
2384
- confidence: result.confidence,
2385
- uncertainty: result.uncertainty,
2386
- routedBy: result.routedBy,
2387
- neuralBackend: result.neuralBackend,
2388
- costMultiplier: result.costMultiplier,
2389
- reasoning: result.reasoning,
2390
- // iter 45 — ensemble disagreement diagnostic (always set when both
2391
- // unified KRR + bucket specialist are loaded; observable signal for
2392
- // tuning iter 44's threshold). null when not applicable.
2393
- ensembleDisagreement: neuralResult?.ensembleDisagreement ?? null,
2394
- },
2395
- alternatives: result.alternatives,
2396
- backend: {
2397
- enabled: status.enabled,
2398
- available: status.available,
2399
- routedBy: status.routedBy,
2400
- reason: status.reason,
2401
- },
2402
- activeEnv: {
2403
- CLAUDE_FLOW_ROUTER_NEURAL: process.env.CLAUDE_FLOW_ROUTER_NEURAL ?? null,
2404
- CLAUDE_FLOW_ROUTER_CALIBRATE: process.env.CLAUDE_FLOW_ROUTER_CALIBRATE ?? null,
2405
- CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK: process.env.CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK ?? null,
2406
- CLAUDE_FLOW_ROUTER_LATENCY_BUDGET_MS: process.env.CLAUDE_FLOW_ROUTER_LATENCY_BUDGET_MS ?? null,
2407
- CLAUDE_FLOW_ROUTER_QUALITY_BAR: process.env.CLAUDE_FLOW_ROUTER_QUALITY_BAR ?? null,
2408
- CLAUDE_FLOW_ROUTER_BANDIT_PER_MODEL: process.env.CLAUDE_FLOW_ROUTER_BANDIT_PER_MODEL ?? null,
2409
- CLAUDE_FLOW_ROUTER_ENSEMBLE_UNCERTAINTY_THRESHOLD: process.env.CLAUDE_FLOW_ROUTER_ENSEMBLE_UNCERTAINTY_THRESHOLD ?? null,
2410
- },
2411
- elapsedMs: Math.round(ms * 100) / 100,
2412
- };
2413
- if (fmt === 'json') {
2414
- output.writeln(JSON.stringify(payload, null, 2));
2415
- return { success: true, data: payload };
2416
- }
2417
- output.writeln();
2418
- output.writeln(output.bold('Routing decision preview (ADR-149 iter 30 — no dispatch)'));
2419
- output.writeln(output.dim('─'.repeat(72)));
2420
- output.writeln(` Task: "${payload.task}"`);
2421
- output.writeln(` Length: ${payload.taskLength} chars`);
2422
- output.writeln(` Embedding: ${payload.hasEmbedding ? `${payload.embeddingDim} dims` : output.warning('not available (embedder absent / disabled)')}`);
2423
- output.writeln('');
2424
- output.writeln(output.bold(' Complexity:'));
2425
- output.writeln(` score: ${complexity.score.toFixed(3)}`);
2426
- output.writeln(` bucket: ${payload.complexity.bucket}`);
2427
- output.writeln(` features: ${Object.entries(complexity.features).map(([k, v]) => `${k}=${typeof v === 'number' ? v.toFixed(2) : v}`).join(', ')}`);
2428
- if (complexity.indicators.high.length + complexity.indicators.medium.length + complexity.indicators.low.length > 0) {
2429
- output.writeln(` indicators: high=[${complexity.indicators.high.slice(0, 4).join(',')}] medium=[${complexity.indicators.medium.slice(0, 4).join(',')}] low=[${complexity.indicators.low.slice(0, 4).join(',')}]`);
2430
- }
2431
- output.writeln('');
2432
- output.writeln(output.bold(' Decision:'));
2433
- output.writeln(` model: ${output.success(result.model)}${result.modelId ? ` (id=${result.modelId})` : ''}`);
2434
- output.writeln(` routed_by: ${result.routedBy}${result.neuralBackend ? ` via ${result.neuralBackend}` : ''}`);
2435
- output.writeln(` confidence: ${result.confidence.toFixed(3)} uncertainty: ${result.uncertainty.toFixed(3)}`);
2436
- output.writeln(` cost mult: ${result.costMultiplier.toFixed(2)}×`);
2437
- if (result.provider === 'openrouter' && result.openrouterModel) {
2438
- output.writeln(` via: openrouter → ${result.openrouterModel}`);
2439
- }
2440
- output.writeln(` reasoning: ${result.reasoning}`);
2441
- if (neuralResult?.ensembleDisagreement !== undefined) {
2442
- const d = neuralResult.ensembleDisagreement;
2443
- const annotation = d > 0.20 ? output.warning(' ⚠ high — consider tuning iter 44 threshold')
2444
- : d > 0.10 ? output.dim(' (moderate)')
2445
- : output.dim(' (low — predictions agree)');
2446
- output.writeln(` ensemble disagreement: ${d.toFixed(4)}${annotation}`);
2447
- }
2448
- output.writeln('');
2449
- if (result.alternatives.length > 0) {
2450
- output.writeln(output.bold(' Alternatives (model: score):'));
2451
- for (const a of result.alternatives) {
2452
- output.writeln(` ${a.model.padEnd(8)} ${a.score.toFixed(4)}`);
2453
- }
2454
- output.writeln('');
2455
- }
2456
- output.writeln(output.bold(' Backend state:'));
2457
- output.writeln(` enabled: ${status.enabled}`);
2458
- output.writeln(` available: ${status.available}`);
2459
- output.writeln(` routedBy: ${status.routedBy ?? '—'}`);
2460
- output.writeln(` reason: ${status.reason}`);
2461
- output.writeln('');
2462
- const activeEnvKeys = Object.entries(payload.activeEnv).filter(([, v]) => v !== null);
2463
- if (activeEnvKeys.length > 0) {
2464
- output.writeln(output.bold(' Active env overrides:'));
2465
- for (const [k, v] of activeEnvKeys)
2466
- output.writeln(` ${k}=${v}`);
2467
- output.writeln('');
2468
- }
2469
- output.writeln(output.dim(` Total decision time: ${ms.toFixed(1)}ms`));
2470
- output.writeln('');
2471
- return { success: true, data: payload };
2472
- },
2473
- };
2474
- // ADR-149 iter 32 — counterfactual cost-savings analysis. Iter 31 added
2475
- // cost_usd to outcome rows; this subcommand consumes that and asks "what
2476
- // would each decision have cost on the heuristic-only path?" — surfacing
2477
- // the actual production savings the router delivers.
2478
- const routerCostSavingsCommand = {
2479
- name: 'cost-savings',
2480
- description: 'Compute actual vs heuristic-counterfactual cost from paired decision+outcome rows (ADR-149 iter 32)',
2481
- options: [
2482
- { name: 'in', short: 'i', type: 'string', description: 'Trajectory JSONL path (default: $CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH or .swarm/...)' },
2483
- { name: 'since', short: 's', type: 'string', description: 'Time window suffix: 1h, 24h, 7d, 30d' },
2484
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
2485
- { name: 'top-n', type: 'number', description: 'Show top-N largest individual savings (default 5)', default: '5' },
2486
- { name: 'baseline', short: 'b', type: 'string', description: 'Counterfactual baseline: heuristic | always-haiku | always-sonnet | always-opus | always-gpt-4.1 | all (default: all)', default: 'all' },
2487
- { name: 'window', short: 'w', type: 'string', description: 'Bin decisions into successive windows of this duration (e.g. 1h, 24h, 7d). Output adds a trend table — iter 34 drift detection.' },
2488
- { name: 'task-hash', type: 'string', description: 'Filter to specific task_hash (FNV-1a-32 hex). For per-task cost investigation — iter 61.' },
2489
- { name: 'alert-on-drop-pct', type: 'number', description: 'Exit 1 if the most recent window\'s savings% falls > N points below the mean of prior windows. Requires --window. Default off. (ADR-149 iter 50)' },
2490
- ],
2491
- examples: [
2492
- { command: 'claude-flow neural router cost-savings', description: 'All-time, all baselines (heuristic + Sonnet-always + Opus-always)' },
2493
- { command: 'claude-flow neural router cost-savings --baseline always-sonnet', description: 'Compare only against Sonnet-always' },
2494
- { command: 'claude-flow neural router cost-savings --window 24h', description: 'Daily savings trend — iter 34 drift detection' },
2495
- { command: 'claude-flow neural router cost-savings --since 7d --format json | jq .baselines.heuristic.savings.totalUsd', description: 'Pipe-friendly headline' },
2496
- ],
2497
- action: async (ctx) => {
2498
- const fs = await import('node:fs');
2499
- const path = await import('node:path');
2500
- const { MODEL_PRICES } = await import('../ruvector/model-prices.js');
2501
- const inPath = ctx.flags.in
2502
- ?? process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH
2503
- ?? path.resolve(process.cwd(), '.swarm', 'model-router-trajectories.jsonl');
2504
- const since = ctx.flags.since;
2505
- const fmt = ctx.flags.format || 'table';
2506
- const topN = parseInt((ctx.flags['top-n'] ?? ctx.flags.topN) || '5', 10) || 5;
2507
- if (!fs.existsSync(inPath)) {
2508
- const msg = `Trajectory file not found at ${inPath}`;
2509
- if (fmt === 'json') {
2510
- output.writeln(JSON.stringify({ error: msg }, null, 2));
2511
- }
2512
- else {
2513
- output.printError(msg);
2514
- }
2515
- return { success: false, exitCode: 1 };
2516
- }
2517
- // iter 62 — preserve ALL outcomes (don't dedup by task_hash). Decisions
2518
- // can still dedup because all occurrences of the same task have the
2519
- // same embedding/complexity (it's the same task!), but different runs
2520
- // produce different token counts and costs. The Array preserves those.
2521
- const decisions = new Map();
2522
- const outcomes = [];
2523
- let malformed = 0;
2524
- for (const l of fs.readFileSync(inPath, 'utf8').split('\n')) {
2525
- if (!l.trim())
2526
- continue;
2527
- try {
2528
- const r = JSON.parse(l);
2529
- if (r.type === 'decision')
2530
- decisions.set(r.task_hash, r);
2531
- else if (r.type === 'outcome')
2532
- outcomes.push(r);
2533
- }
2534
- catch {
2535
- malformed++;
2536
- }
2537
- }
2538
- // Time-window filter (on the OUTCOME ts, since that's when cost was incurred).
2539
- let cutoffMs = null;
2540
- if (since) {
2541
- const m = since.match(/^(\d+)([hdmw])$/);
2542
- if (m) {
2543
- const n = parseInt(m[1], 10);
2544
- const unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000 }[m[2]] ?? 0;
2545
- cutoffMs = Date.now() - n * unitMs;
2546
- }
2547
- }
2548
- // iter 33 — multi-baseline counterfactuals. Each per-call row carries
2549
- // the actual cost AND a map of counterfactual costs (one per baseline),
2550
- // so a single LOO over the trajectory data computes all baselines at once.
2551
- const baselineArg = ctx.flags.baseline ?? 'all';
2552
- const allBaselines = ['heuristic', 'always-haiku', 'always-sonnet', 'always-opus', 'always-gpt-4.1'];
2553
- const baselines = baselineArg === 'all' ? allBaselines : [baselineArg];
2554
- // Map each baseline label → the modelId used for pricing.
2555
- const baselineModelFor = (baseline, dec) => {
2556
- switch (baseline) {
2557
- case 'heuristic':
2558
- return dec.ab_pair?.bandit_pick
2559
- ?? (dec.complexity < 0.34 ? 'haiku' : dec.complexity < 0.67 ? 'sonnet' : 'opus');
2560
- case 'always-haiku': return 'haiku';
2561
- case 'always-sonnet': return 'sonnet';
2562
- case 'always-opus': return 'opus';
2563
- case 'always-gpt-4.1': return 'openai/gpt-4.1';
2564
- default: return baseline; // operator passes raw modelId
2565
- }
2566
- };
2567
- const perCall = [];
2568
- let droppedNoOutcomeCost = 0;
2569
- let droppedNoDecision = 0;
2570
- let droppedNoTokens = 0;
2571
- // iter 61 — task-hash filter for per-task cost investigation.
2572
- const taskHashFilter = (ctx.flags['task-hash'] ?? ctx.flags.taskHash)?.toLowerCase();
2573
- if (taskHashFilter && !/^[0-9a-f]{8}$/.test(taskHashFilter)) {
2574
- output.printError(`--task-hash must be an 8-char hex string (got "${taskHashFilter}")`);
2575
- return { success: false, exitCode: 1 };
2576
- }
2577
- // iter 62 — iterate ALL outcomes (Array), not just one per hash. Multiple
2578
- // occurrences of the same task contribute separately to the aggregate.
2579
- for (const out of outcomes) {
2580
- if (cutoffMs !== null && Date.parse(out.ts) < cutoffMs)
2581
- continue;
2582
- if (taskHashFilter && out.task_hash !== taskHashFilter)
2583
- continue;
2584
- if (out.cost_usd == null) {
2585
- droppedNoOutcomeCost++;
2586
- continue;
2587
- }
2588
- const dec = decisions.get(out.task_hash);
2589
- if (!dec) {
2590
- droppedNoDecision++;
2591
- continue;
2592
- }
2593
- if (!out.tokens) {
2594
- droppedNoTokens++;
2595
- continue;
2596
- }
2597
- const actualCost = out.cost_usd;
2598
- const actualModel = out.model_id ?? dec.openrouter_model ?? dec.model;
2599
- const tier = dec.complexity < 0.34 ? 'cheap' : dec.complexity < 0.67 ? 'mid' : 'strong';
2600
- // Compute every baseline at once. Cost = (input × $/Mtok_in + output ×
2601
- // $/Mtok_out) / 1e6 — same formula iter 31's costUsd() uses.
2602
- const counterfactuals = {};
2603
- for (const b of baselines) {
2604
- const m = baselineModelFor(b, dec);
2605
- const p = MODEL_PRICES[m] ?? { in: 1, out: 1 };
2606
- const cost = (out.tokens.input * p.in + out.tokens.output * p.out) / 1_000_000;
2607
- counterfactuals[b] = { model: m, cost, savings: cost - actualCost };
2608
- }
2609
- perCall.push({
2610
- task_hash: out.task_hash, ts: out.ts,
2611
- actualModel, actualCost,
2612
- counterfactuals,
2613
- tokens: out.tokens,
2614
- complexity: dec.complexity, tier,
2615
- });
2616
- }
2617
- const round6 = (x) => Math.round(x * 1_000_000) / 1_000_000;
2618
- const round2 = (x) => Math.round(x * 100) / 100;
2619
- const totalActual = perCall.reduce((s, p) => s + p.actualCost, 0);
2620
- const baselineAggs = {};
2621
- for (const b of baselines) {
2622
- let totalCf = 0;
2623
- const byTier = {
2624
- cheap: { actual: 0, counterfactual: 0, n: 0 },
2625
- mid: { actual: 0, counterfactual: 0, n: 0 },
2626
- strong: { actual: 0, counterfactual: 0, n: 0 },
2627
- };
2628
- for (const c of perCall) {
2629
- const cf = c.counterfactuals[b]?.cost ?? 0;
2630
- totalCf += cf;
2631
- byTier[c.tier].actual += c.actualCost;
2632
- byTier[c.tier].counterfactual += cf;
2633
- byTier[c.tier].n += 1;
2634
- }
2635
- const savings = totalCf - totalActual;
2636
- const pct = totalCf > 0 ? (savings / totalCf) * 100 : 0;
2637
- baselineAggs[b] = {
2638
- totalUsd: round6(savings),
2639
- savingsPct: round2(pct),
2640
- actualUsd: round6(totalActual),
2641
- counterfactualUsd: round6(totalCf),
2642
- byTier: Object.fromEntries(Object.entries(byTier).map(([k, v]) => [k, {
2643
- n: v.n,
2644
- actualUsd: round6(v.actual),
2645
- counterfactualUsd: round6(v.counterfactual),
2646
- savingsUsd: round6(v.counterfactual - v.actual),
2647
- savingsPct: v.counterfactual > 0 ? round2(((v.counterfactual - v.actual) / v.counterfactual) * 100) : 0,
2648
- }])),
2649
- };
2650
- }
2651
- // Top-N largest individual savings — use the first baseline's per-call savings.
2652
- const primaryBaseline = baselines[0];
2653
- const topSavings = [...perCall].sort((a, b) => (b.counterfactuals[primaryBaseline]?.savings ?? 0) - (a.counterfactuals[primaryBaseline]?.savings ?? 0)).slice(0, topN);
2654
- const payload = {
2655
- input: inPath,
2656
- filters: { since, taskHash: taskHashFilter },
2657
- pairs: perCall.length,
2658
- dropped: { noOutcomeCost: droppedNoOutcomeCost, noDecision: droppedNoDecision, noTokens: droppedNoTokens },
2659
- baselines: Object.fromEntries(Object.entries(baselineAggs).map(([k, v]) => [k, {
2660
- savings: { totalUsd: v.totalUsd, savingsPct: v.savingsPct, actualUsd: v.actualUsd, counterfactualUsd: v.counterfactualUsd },
2661
- byTier: v.byTier,
2662
- }])),
2663
- // Back-compat: top-level `savings` and `byTier` mirror the PRIMARY baseline
2664
- // (first in the requested list, which for --baseline all is "heuristic" — same
2665
- // as iter 32's output shape). Iter 32 callers parsing the old shape keep working.
2666
- savings: baselineAggs[primaryBaseline]
2667
- ? { totalUsd: baselineAggs[primaryBaseline].totalUsd, savingsPct: baselineAggs[primaryBaseline].savingsPct, actualUsd: baselineAggs[primaryBaseline].actualUsd, counterfactualUsd: baselineAggs[primaryBaseline].counterfactualUsd }
2668
- : { totalUsd: 0, savingsPct: 0, actualUsd: round6(totalActual), counterfactualUsd: 0 },
2669
- byTier: baselineAggs[primaryBaseline]?.byTier ?? {},
2670
- topSavings: topSavings.map(t => ({
2671
- ts: t.ts, actualModel: t.actualModel,
2672
- counterfactualModel: t.counterfactuals[primaryBaseline]?.model ?? '—',
2673
- actualUsd: round6(t.actualCost),
2674
- counterfactualUsd: round6(t.counterfactuals[primaryBaseline]?.cost ?? 0),
2675
- savingsUsd: round6(t.counterfactuals[primaryBaseline]?.savings ?? 0),
2676
- })),
2677
- };
2678
- // iter 34 — windowed drift detection. When --window is set, bin the
2679
- // paired calls into successive duration windows and emit a trend table
2680
- // (one row per window). Useful for "is the router degrading over time?"
2681
- // — a sudden drop in savings %% across windows surfaces calibration drift,
2682
- // workload shifts, or model deprecation.
2683
- const windowArg = ctx.flags.window;
2684
- let windowedTrend;
2685
- if (windowArg) {
2686
- const m = windowArg.match(/^(\d+)([hdmw])$/);
2687
- if (!m) {
2688
- output.printError(`--window must match Nh|Nd|Nm|Nw (got ${windowArg})`);
2689
- return { success: false, exitCode: 1 };
2690
- }
2691
- const n = parseInt(m[1], 10);
2692
- const unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000 }[m[2]] ?? 0;
2693
- const windowMs = n * unitMs;
2694
- if (windowMs <= 0) {
2695
- output.printError(`--window must be positive (got ${windowArg})`);
2696
- return { success: false, exitCode: 1 };
2697
- }
2698
- // Bin perCall by (outcome.ts - earliest.ts) / windowMs.
2699
- // Sort first so window indices are monotonic and gaps are visible.
2700
- const sorted = [...perCall].sort((a, b) => a.ts.localeCompare(b.ts));
2701
- if (sorted.length === 0) {
2702
- windowedTrend = [];
2703
- }
2704
- else {
2705
- const earliestMs = Date.parse(sorted[0].ts);
2706
- const bins = new Map();
2707
- for (const c of sorted) {
2708
- const idx = Math.floor((Date.parse(c.ts) - earliestMs) / windowMs);
2709
- if (!bins.has(idx))
2710
- bins.set(idx, []);
2711
- bins.get(idx).push(c);
2712
- }
2713
- const rows = [];
2714
- let priorPct = null;
2715
- for (const idx of [...bins.keys()].sort((a, b) => a - b)) {
2716
- const items = bins.get(idx);
2717
- let actual = 0, cf = 0;
2718
- for (const c of items) {
2719
- actual += c.actualCost;
2720
- cf += c.counterfactuals[primaryBaseline]?.cost ?? 0;
2721
- }
2722
- const savings = cf - actual;
2723
- const pct = cf > 0 ? (savings / cf) * 100 : 0;
2724
- const windowStart = new Date(earliestMs + idx * windowMs).toISOString();
2725
- const windowEnd = new Date(earliestMs + (idx + 1) * windowMs - 1).toISOString();
2726
- const deltaVsPriorPct = priorPct === null ? null : round2(pct - priorPct);
2727
- priorPct = pct;
2728
- rows.push({
2729
- windowStart, windowEnd, n: items.length,
2730
- actualUsd: round6(actual),
2731
- counterfactualUsd: round6(cf),
2732
- savingsUsd: round6(savings),
2733
- savingsPct: round2(pct),
2734
- deltaVsPriorPct,
2735
- });
2736
- }
2737
- windowedTrend = rows;
2738
- }
2739
- payload.windowedTrend = windowedTrend;
2740
- payload.windowConfig = { duration: windowArg, primaryBaseline };
2741
- }
2742
- // iter 50 — drift alert. When --alert-on-drop-pct is set AND --window
2743
- // produced ≥ 2 windows, compare the most-recent window's savings% to
2744
- // the mean of prior windows'. If it dropped by > threshold, fail
2745
- // (exit 1) so monitoring catches it. Independent of fmt — alert
2746
- // state goes into the payload AND drives the exit code.
2747
- const alertDropPctArg = (ctx.flags['alert-on-drop-pct'] ?? ctx.flags.alertOnDropPct);
2748
- let alertTriggered = false;
2749
- let alertReason = null;
2750
- if (alertDropPctArg !== undefined && alertDropPctArg !== null && alertDropPctArg !== '') {
2751
- const dropThreshold = typeof alertDropPctArg === 'string' ? parseFloat(alertDropPctArg) : alertDropPctArg;
2752
- if (!isFinite(dropThreshold) || dropThreshold <= 0) {
2753
- output.printError(`--alert-on-drop-pct must be a positive number (got ${alertDropPctArg})`);
2754
- return { success: false, exitCode: 1 };
2755
- }
2756
- if (!windowedTrend || windowedTrend.length < 2) {
2757
- alertReason = `not enough windows for drift detection (need ≥ 2, got ${windowedTrend?.length ?? 0}) — alert skipped`;
2758
- }
2759
- else {
2760
- const latest = windowedTrend[windowedTrend.length - 1];
2761
- const prior = windowedTrend.slice(0, -1);
2762
- const priorMean = prior.reduce((s, w) => s + w.savingsPct, 0) / prior.length;
2763
- const dropPct = priorMean - latest.savingsPct;
2764
- if (dropPct > dropThreshold) {
2765
- alertTriggered = true;
2766
- alertReason = `latest window savings ${latest.savingsPct.toFixed(2)}% is ${dropPct.toFixed(2)} points BELOW prior windows' mean ${priorMean.toFixed(2)}% (threshold ${dropThreshold})`;
2767
- }
2768
- else {
2769
- alertReason = `latest window savings ${latest.savingsPct.toFixed(2)}% within ${dropThreshold} points of prior mean ${priorMean.toFixed(2)}% — OK`;
2770
- }
2771
- }
2772
- payload.alert = {
2773
- triggered: alertTriggered,
2774
- reason: alertReason,
2775
- dropThreshold,
2776
- };
2777
- }
2778
- if (fmt === 'json') {
2779
- output.writeln(JSON.stringify(payload, null, 2));
2780
- return alertTriggered
2781
- ? { success: false, exitCode: 1, data: payload }
2782
- : { success: true, data: payload };
2783
- }
2784
- output.writeln();
2785
- output.writeln(output.bold('Cost-savings analysis (ADR-149 iter 32)'));
2786
- output.writeln(output.dim('─'.repeat(72)));
2787
- output.writeln(` Input: ${inPath}`);
2788
- if (since)
2789
- output.writeln(` Time window: since ${since}`);
2790
- if (taskHashFilter)
2791
- output.writeln(` Task hash: ${taskHashFilter} (filtered to this task)`);
2792
- output.writeln(` Paired calls: ${perCall.length} (dropped: no-cost=${droppedNoOutcomeCost}, no-decision=${droppedNoDecision}, no-tokens=${droppedNoTokens})`);
2793
- output.writeln('');
2794
- if (perCall.length === 0) {
2795
- output.writeln(output.dim(' No cost-bearing paired rows. Enable trajectory recording AND make sure'));
2796
- output.writeln(output.dim(' outcome rows include `tokens` (iter 31 wired this through agent-execute-core).'));
2797
- output.writeln('');
2798
- return { success: true, data: payload };
2799
- }
2800
- output.writeln(output.bold(' Headline (actual = $' + totalActual.toFixed(6) + '):'));
2801
- output.writeln(' baseline counterfactual savings %');
2802
- for (const [b, agg] of Object.entries(baselineAggs)) {
2803
- const savingsStr = agg.totalUsd >= 0 ? '$' + agg.totalUsd.toFixed(6) : '-$' + Math.abs(agg.totalUsd).toFixed(6);
2804
- const colored = agg.totalUsd >= 0 ? output.success(savingsStr) : output.warning(savingsStr);
2805
- output.writeln(` ${b.padEnd(18)} $${agg.counterfactualUsd.toFixed(6).padEnd(14)} ${colored.padEnd(20)} ${agg.savingsPct.toString().padStart(6)}%`);
2806
- }
2807
- output.writeln('');
2808
- output.writeln(output.dim(` Primary baseline for per-tier and top-savings views: "${primaryBaseline}"`));
2809
- output.writeln('');
2810
- const primaryAgg = baselineAggs[primaryBaseline];
2811
- output.writeln(output.bold(' By tier (' + primaryBaseline + '):'));
2812
- output.writeln(' tier n actual counterfactual savings %');
2813
- for (const [k, v] of Object.entries(primaryAgg.byTier)) {
2814
- output.writeln(` ${k.padEnd(8)} ${String(v.n).padStart(3)} $${v.actualUsd.toFixed(6).padEnd(14)} $${v.counterfactualUsd.toFixed(6).padEnd(14)} $${v.savingsUsd.toFixed(6).padEnd(14)} ${v.savingsPct.toString().padStart(6)}%`);
2815
- }
2816
- output.writeln('');
2817
- if (topSavings.length > 0) {
2818
- output.writeln(output.bold(` Top ${topSavings.length} largest individual savings (${primaryBaseline}):`));
2819
- output.writeln(' ts actual → counterfactual saved');
2820
- for (const t of topSavings) {
2821
- const cfModel = t.counterfactuals[primaryBaseline]?.model ?? '—';
2822
- const sv = t.counterfactuals[primaryBaseline]?.savings ?? 0;
2823
- output.writeln(` ${t.ts.slice(0, 19)} ${t.actualModel.padEnd(40)} → ${cfModel.padEnd(40)} $${sv.toFixed(6)}`);
2824
- }
2825
- output.writeln('');
2826
- }
2827
- // iter 34 — windowed trend table for drift detection.
2828
- if (windowedTrend && windowedTrend.length > 0) {
2829
- output.writeln(output.bold(` Windowed trend (${windowArg} bins, baseline=${primaryBaseline}):`));
2830
- output.writeln(' window start n actual counterfactual savings % Δ% vs prior');
2831
- for (const w of windowedTrend) {
2832
- const arrow = w.deltaVsPriorPct === null ? ''
2833
- : w.deltaVsPriorPct > 0 ? output.success(`↑ +${w.deltaVsPriorPct.toFixed(2)}`)
2834
- : w.deltaVsPriorPct < 0 ? output.warning(`↓ ${w.deltaVsPriorPct.toFixed(2)}`)
2835
- : '·';
2836
- output.writeln(` ${w.windowStart.slice(0, 19)} ${String(w.n).padStart(3)} $${w.actualUsd.toFixed(6).padEnd(11)} $${w.counterfactualUsd.toFixed(6).padEnd(14)} $${w.savingsUsd.toFixed(6).padEnd(11)} ${w.savingsPct.toString().padStart(6)}% ${arrow}`);
2837
- }
2838
- output.writeln('');
2839
- output.writeln(output.dim(' Δ% vs prior: change in savings % from the prior window. Large negative'));
2840
- output.writeln(output.dim(' deltas suggest router degradation, workload shift, or calibration drift.'));
2841
- output.writeln('');
2842
- }
2843
- // iter 50 — alert footer when --alert-on-drop-pct was used.
2844
- if (alertReason !== null) {
2845
- if (alertTriggered) {
2846
- output.writeln(output.warning(` ⚠ ALERT: ${alertReason}`));
2847
- }
2848
- else {
2849
- output.writeln(output.dim(` ${alertReason}`));
2850
- }
2851
- output.writeln('');
2852
- }
2853
- return alertTriggered
2854
- ? { success: false, exitCode: 1, data: payload }
2855
- : { success: true, data: payload };
2856
- },
2857
- };
2858
- // ADR-149 iter 36 — operational observability for the trajectory JSONL itself.
2859
- // Iter 17 added the recorder with rotation. Iter 28/30/32 consume the data.
2860
- // Nothing previously surfaced "is logging healthy?" — size vs cap, rotation
2861
- // count, parse success rate, pair-join rate, time range. SREs need this view.
2862
- const routerTrajectoryHealthCommand = {
2863
- name: 'trajectory-health',
2864
- description: 'Show health of the routing-decision JSONL log: size, rotations, parse rate, pair-join rate (ADR-149 iter 36)',
2865
- options: [
2866
- { name: 'in', short: 'i', type: 'string', description: 'Trajectory JSONL path (default: $CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH or .swarm/...)' },
2867
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
2868
- ],
2869
- examples: [
2870
- { command: 'claude-flow neural router trajectory-health', description: 'Snapshot of trajectory log health' },
2871
- { command: 'claude-flow neural router trajectory-health --format json | jq .pairJoinRatePct', description: 'Pipe-friendly pair-join rate for dashboards' },
2872
- ],
2873
- action: async (ctx) => {
2874
- const fs = await import('node:fs');
2875
- const path = await import('node:path');
2876
- const inPath = ctx.flags.in
2877
- ?? process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH
2878
- ?? path.resolve(process.cwd(), '.swarm', 'model-router-trajectories.jsonl');
2879
- const fmt = ctx.flags.format || 'table';
2880
- // Recorder config (mirrors router-trajectory.ts defaults).
2881
- const recorderEnabled = process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY === '1';
2882
- const maxSizeBytes = parseInt(process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_MAXSIZE ?? `${10 * 1024 * 1024}`, 10) | 0;
2883
- const maxRotations = Math.max(0, parseInt(process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_MAXROTATIONS ?? '3', 10) || 3);
2884
- if (!fs.existsSync(inPath)) {
2885
- const payload = {
2886
- recorderEnabled,
2887
- input: inPath,
2888
- exists: false,
2889
- message: 'No trajectory file at the configured path.',
2890
- hint: recorderEnabled
2891
- ? 'Recorder is enabled — file should appear after the next routing decision.'
2892
- : 'Recorder is OFF. Set CLAUDE_FLOW_ROUTER_TRAJECTORY=1 to enable.',
2893
- };
2894
- if (fmt === 'json')
2895
- output.writeln(JSON.stringify(payload, null, 2));
2896
- else {
2897
- output.writeln('');
2898
- output.writeln(output.bold('Trajectory health'));
2899
- output.writeln(output.dim('─'.repeat(60)));
2900
- output.writeln(` Path: ${inPath}`);
2901
- output.writeln(` Recorder gate: ${recorderEnabled ? output.success('ON') : output.warning('OFF (CLAUDE_FLOW_ROUTER_TRAJECTORY=1 to enable)')}`);
2902
- output.writeln(` Status: ${output.warning('file does not exist')}`);
2903
- output.writeln(` Hint: ${payload.hint}`);
2904
- output.writeln('');
2905
- }
2906
- return { success: true, data: payload };
2907
- }
2908
- const stat = fs.statSync(inPath);
2909
- const sizeBytes = stat.size;
2910
- const sizePct = maxSizeBytes > 0 ? Math.round((sizeBytes / maxSizeBytes) * 1000) / 10 : 0;
2911
- // Count .bak rotation files.
2912
- const rotationFiles = [];
2913
- for (let i = 1; i <= maxRotations; i++) {
2914
- const p = `${inPath}.${i}`;
2915
- if (fs.existsSync(p)) {
2916
- const s = fs.statSync(p);
2917
- rotationFiles.push({ index: i, path: p, bytes: s.size, mtime: s.mtime.toISOString() });
2918
- }
2919
- }
2920
- const lines = fs.readFileSync(inPath, 'utf8').split('\n').filter(l => l.trim().length > 0);
2921
- let decisions = 0, outcomes = 0, malformed = 0, otherType = 0;
2922
- let oldestTs = null;
2923
- let newestTs = null;
2924
- const decisionHashes = new Set();
2925
- const outcomeHashes = new Set();
2926
- for (const l of lines) {
2927
- try {
2928
- const r = JSON.parse(l);
2929
- if (r.type === 'decision') {
2930
- decisions++;
2931
- if (r.task_hash)
2932
- decisionHashes.add(r.task_hash);
2933
- }
2934
- else if (r.type === 'outcome') {
2935
- outcomes++;
2936
- if (r.task_hash)
2937
- outcomeHashes.add(r.task_hash);
2938
- }
2939
- else {
2940
- otherType++;
2941
- }
2942
- if (r.ts) {
2943
- if (oldestTs === null || r.ts < oldestTs)
2944
- oldestTs = r.ts;
2945
- if (newestTs === null || r.ts > newestTs)
2946
- newestTs = r.ts;
2947
- }
2948
- }
2949
- catch {
2950
- malformed++;
2951
- }
2952
- }
2953
- // Pair-join rate: % of decisions that have a matching outcome.
2954
- let pairedHashes = 0;
2955
- for (const h of decisionHashes)
2956
- if (outcomeHashes.has(h))
2957
- pairedHashes++;
2958
- const pairJoinRatePct = decisionHashes.size > 0
2959
- ? Math.round((pairedHashes / decisionHashes.size) * 10000) / 100
2960
- : 0;
2961
- const parseSuccessPct = lines.length > 0
2962
- ? Math.round(((lines.length - malformed) / lines.length) * 10000) / 100
2963
- : 100;
2964
- const payload = {
2965
- recorderEnabled,
2966
- input: inPath,
2967
- exists: true,
2968
- file: {
2969
- sizeBytes,
2970
- sizePct,
2971
- maxSizeBytes,
2972
- maxRotations,
2973
- rotationsOnDisk: rotationFiles.length,
2974
- rotations: rotationFiles,
2975
- mtime: stat.mtime.toISOString(),
2976
- },
2977
- rows: {
2978
- total: lines.length,
2979
- decisions, outcomes, otherType, malformed,
2980
- },
2981
- pairing: {
2982
- uniqueDecisionHashes: decisionHashes.size,
2983
- uniqueOutcomeHashes: outcomeHashes.size,
2984
- pairedHashes,
2985
- pairJoinRatePct,
2986
- parseSuccessPct,
2987
- },
2988
- timeRange: {
2989
- oldestTs, newestTs,
2990
- spanHours: oldestTs && newestTs ? Math.round(((Date.parse(newestTs) - Date.parse(oldestTs)) / 3600_000) * 10) / 10 : 0,
2991
- },
2992
- };
2993
- if (fmt === 'json') {
2994
- output.writeln(JSON.stringify(payload, null, 2));
2995
- return { success: true, data: payload };
2996
- }
2997
- output.writeln();
2998
- output.writeln(output.bold('Trajectory health (ADR-149 iter 36)'));
2999
- output.writeln(output.dim('─'.repeat(60)));
3000
- output.writeln(` Path: ${inPath}`);
3001
- output.writeln(` Recorder gate: ${recorderEnabled ? output.success('ON') : output.warning('OFF')}`);
3002
- output.writeln('');
3003
- output.writeln(output.bold(' File:'));
3004
- const sizeMb = (sizeBytes / 1_048_576).toFixed(3);
3005
- const capMb = (maxSizeBytes / 1_048_576).toFixed(1);
3006
- const sizeWarn = sizePct >= 80 ? output.warning(`(${sizePct}% of cap)`) : output.dim(`(${sizePct}% of cap)`);
3007
- output.writeln(` size: ${sizeBytes} bytes (${sizeMb} MB) of ${capMb} MB max ${sizeWarn}`);
3008
- output.writeln(` last write: ${stat.mtime.toISOString()}`);
3009
- output.writeln(` rotations: ${rotationFiles.length} of ${maxRotations} max .bak files on disk`);
3010
- for (const r of rotationFiles) {
3011
- output.writeln(` .${r.index}: ${r.bytes} bytes ${r.mtime}`);
3012
- }
3013
- output.writeln('');
3014
- output.writeln(output.bold(' Rows:'));
3015
- output.writeln(` total: ${lines.length}`);
3016
- output.writeln(` decisions: ${decisions}`);
3017
- output.writeln(` outcomes: ${outcomes}`);
3018
- if (otherType > 0)
3019
- output.writeln(` other type: ${otherType}`);
3020
- output.writeln(` malformed: ${malformed} (parse success ${parseSuccessPct}%)`);
3021
- output.writeln('');
3022
- output.writeln(output.bold(' Pairing (decision ↔ outcome join by task_hash):'));
3023
- output.writeln(` unique decision hashes: ${decisionHashes.size}`);
3024
- output.writeln(` unique outcome hashes: ${outcomeHashes.size}`);
3025
- output.writeln(` paired: ${pairedHashes} (${pairJoinRatePct}%)`);
3026
- if (pairJoinRatePct < 50 && decisionHashes.size > 5) {
3027
- output.writeln(output.warning(` ⚠ pair-join rate < 50% — outcome rows may not be wired through (iter 17/31).`));
3028
- }
3029
- output.writeln('');
3030
- if (oldestTs && newestTs) {
3031
- output.writeln(output.bold(' Time range:'));
3032
- output.writeln(` oldest: ${oldestTs}`);
3033
- output.writeln(` newest: ${newestTs}`);
3034
- output.writeln(` span: ${payload.timeRange.spanHours} hours`);
3035
- output.writeln('');
3036
- }
3037
- return { success: true, data: payload };
3038
- },
3039
- };
3040
- // ADR-149 iter 54 — consolidated env-var inspection. The router has ~15
3041
- // CLAUDE_FLOW_ROUTER_* env vars accumulated across iters 12-53. Operators
3042
- // need a single command that lists each with its current value (or default),
3043
- // effect, and which iter introduced it. Color-coded: green = override set,
3044
- // dim = default.
3045
- const routerConfigCommand = {
3046
- name: 'config',
3047
- description: 'List all CLAUDE_FLOW_ROUTER_* env vars with current values + effects (ADR-149 iter 54)',
3048
- options: [
3049
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
3050
- { name: 'only-overrides', type: 'boolean', description: 'Only show env vars that are explicitly set (hide defaults)', default: false },
3051
- ],
3052
- examples: [
3053
- { command: 'claude-flow neural router config', description: 'All router env vars with current values' },
3054
- { command: 'claude-flow neural router config --only-overrides', description: 'Just what the operator has set' },
3055
- { command: 'claude-flow neural router config --format json | jq', description: 'Audit / diff against another deployment' },
3056
- ],
3057
- action: async (ctx) => {
3058
- const fmt = ctx.flags.format || 'table';
3059
- const onlyOverrides = Boolean(ctx.flags['only-overrides'] ?? ctx.flags.onlyOverrides);
3060
- const rows = [
3061
- // Core gate (iter 0)
3062
- { name: 'CLAUDE_FLOW_ROUTER_NEURAL', iter: 0, defaultValue: 'unset (0)', currentValue: process.env.CLAUDE_FLOW_ROUTER_NEURAL ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_NEURAL, effect: 'Gate. =1 enables neural router; otherwise pure-bandit heuristic.' },
3063
- { name: 'CLAUDE_FLOW_ROUTER_MODEL_PATH', iter: 0, defaultValue: 'unset', currentValue: process.env.CLAUDE_FLOW_ROUTER_MODEL_PATH ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_MODEL_PATH, effect: 'Override the KRR artifact path. Defaults to bundled.' },
3064
- { name: 'CLAUDE_FLOW_ROUTER_QUALITY_BAR', iter: 0, defaultValue: '0.50', currentValue: process.env.CLAUDE_FLOW_ROUTER_QUALITY_BAR ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_QUALITY_BAR, effect: 'Cost-optimal mode: minimum predicted quality to pick a candidate.' },
3065
- // Trajectory (iter 17)
3066
- { name: 'CLAUDE_FLOW_ROUTER_TRAJECTORY', iter: 17, defaultValue: 'unset (0)', currentValue: process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY, effect: 'Gate. =1 writes decision+outcome rows to .swarm/model-router-trajectories.jsonl.' },
3067
- { name: 'CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH', iter: 17, defaultValue: '.swarm/model-router-trajectories.jsonl', currentValue: process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH, effect: 'Override the trajectory JSONL path.' },
3068
- { name: 'CLAUDE_FLOW_ROUTER_TRAJECTORY_MAXSIZE', iter: 17, defaultValue: '10485760 (10MB)', currentValue: process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_MAXSIZE ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_MAXSIZE, effect: 'Bytes before rotation. Set 0 to disable rotation.' },
3069
- { name: 'CLAUDE_FLOW_ROUTER_TRAJECTORY_MAXROTATIONS', iter: 17, defaultValue: '3', currentValue: process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_MAXROTATIONS ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_MAXROTATIONS, effect: 'Max .bak files to keep when rotating.' },
3070
- { name: 'CLAUDE_FLOW_ROUTER_TRAJECTORY_TASKLEN', iter: 17, defaultValue: '500', currentValue: process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_TASKLEN ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_TASKLEN, effect: 'Max chars of task text to persist per row (truncated above this).' },
3071
- // Latency budget (iter 12)
3072
- { name: 'CLAUDE_FLOW_ROUTER_LATENCY_BUDGET_MS', iter: 12, defaultValue: '0 (unbounded)', currentValue: process.env.CLAUDE_FLOW_ROUTER_LATENCY_BUDGET_MS ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_LATENCY_BUDGET_MS, effect: 'Drop candidates whose measured p50 latency > this many ms before selection.' },
3073
- // Per-modelId bandit (iter 14)
3074
- { name: 'CLAUDE_FLOW_ROUTER_BANDIT_PER_MODEL', iter: 14, defaultValue: 'unset (0)', currentValue: process.env.CLAUDE_FLOW_ROUTER_BANDIT_PER_MODEL ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_BANDIT_PER_MODEL, effect: 'Gate. =1 enables per-modelId Thompson sampling perturbation of neural prediction.' },
3075
- // k-NN backend (iter 0)
3076
- { name: 'CLAUDE_FLOW_ROUTER_KNN_K', iter: 0, defaultValue: '5', currentValue: process.env.CLAUDE_FLOW_ROUTER_KNN_K ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_KNN_K, effect: 'k for the k-NN backend when KRR is not loadable.' },
3077
- { name: 'CLAUDE_FLOW_ROUTER_SEED_CORPUS', iter: 0, defaultValue: 'bundled seed-rows.json', currentValue: process.env.CLAUDE_FLOW_ROUTER_SEED_CORPUS ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_SEED_CORPUS, effect: 'Override the DRACO seed corpus path.' },
3078
- // Calibration (iter 22-25)
3079
- { name: 'CLAUDE_FLOW_ROUTER_CALIBRATE', iter: 24, defaultValue: 'unset (default ON)', currentValue: process.env.CLAUDE_FLOW_ROUTER_CALIBRATE ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_CALIBRATE, effect: 'Isotonic calibration of KRR predictions. =0 opts out (recovers raw KRR). Default-on since iter 24 (OOS validated).' },
3080
- { name: 'CLAUDE_FLOW_ROUTER_CALIBRATOR_PATH', iter: 22, defaultValue: 'bundled seed-router.calibrator.json', currentValue: process.env.CLAUDE_FLOW_ROUTER_CALIBRATOR_PATH ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_CALIBRATOR_PATH, effect: 'Override the unified calibrator path. Per-tier files (low/med/high) load from the same dir.' },
3081
- // Cost ceiling (iter 29)
3082
- { name: 'CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK', iter: 29, defaultValue: '0 (disabled)', currentValue: process.env.CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK, effect: 'Orthogonal selector mode: pick BEST quality among candidates ≤ ceiling $/Mtok blended.' },
3083
- // A/B mode (iter 5/37)
3084
- { name: 'CLAUDE_FLOW_ROUTER_AB', iter: 5, defaultValue: 'unset (0)', currentValue: process.env.CLAUDE_FLOW_ROUTER_AB ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_AB, effect: 'Legacy all-on A/B mode. Records bandit_pick + hybrid_pick on every decision.' },
3085
- { name: 'CLAUDE_FLOW_ROUTER_AB_SAMPLE_RATE', iter: 37, defaultValue: '0 (disabled)', currentValue: process.env.CLAUDE_FLOW_ROUTER_AB_SAMPLE_RATE ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_AB_SAMPLE_RATE, effect: 'Sampled A/B mode. 0..1 fraction of decisions to A/B (deterministic by task_hash). Overrides legacy AB=1.' },
3086
- // Ensemble uncertainty (iter 44)
3087
- { name: 'CLAUDE_FLOW_ROUTER_ENSEMBLE_UNCERTAINTY_THRESHOLD', iter: 44, defaultValue: '0 (disabled)', currentValue: process.env.CLAUDE_FLOW_ROUTER_ENSEMBLE_UNCERTAINTY_THRESHOLD ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_ENSEMBLE_UNCERTAINTY_THRESHOLD, effect: 'When > 0: if |unified_q - specialist_q| > threshold for picked model, fall back to bandit.' },
3088
- // Bandit warmup (iter 52/53)
3089
- { name: 'CLAUDE_FLOW_ROUTER_BANDIT_WARMUP_RANGE', iter: 52, defaultValue: '8', currentValue: process.env.CLAUDE_FLOW_ROUTER_BANDIT_WARMUP_RANGE ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_BANDIT_WARMUP_RANGE, effect: 'Continuous warmup denominator. Smaller = bandit ramps faster; larger = more conservative.' },
3090
- { name: 'CLAUDE_FLOW_ROUTER_BANDIT_FULL_INFLUENCE', iter: 53, defaultValue: 'unset (0)', currentValue: process.env.CLAUDE_FLOW_ROUTER_BANDIT_FULL_INFLUENCE ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_BANDIT_FULL_INFLUENCE, effect: 'Gate. =1 uses asymptotic curve (samples-2)/(samples+WARMUP) — bandit dominates at scale.' },
3091
- { name: 'CLAUDE_FLOW_ROUTER_BANDIT_SHRINKAGE_LAMBDA', iter: 57, defaultValue: '4', currentValue: process.env.CLAUDE_FLOW_ROUTER_BANDIT_SHRINKAGE_LAMBDA ?? '', isOverride: !!process.env.CLAUDE_FLOW_ROUTER_BANDIT_SHRINKAGE_LAMBDA, effect: 'Cross-bucket shrinkage strength. λ=0 disables; higher λ = more bias toward marginal anchor for cold cells.' },
3092
- ];
3093
- const visible = onlyOverrides ? rows.filter(r => r.isOverride) : rows;
3094
- if (fmt === 'json') {
3095
- output.writeln(JSON.stringify(visible, null, 2));
3096
- return { success: true, data: visible };
3097
- }
3098
- output.writeln();
3099
- output.writeln(output.bold('Router config — ADR-149 iter 54 (env-var inventory)'));
3100
- output.writeln(output.dim('─'.repeat(72)));
3101
- output.writeln(` ${visible.length} of ${rows.length} entries (${rows.filter(r => r.isOverride).length} overridden, ${rows.length - rows.filter(r => r.isOverride).length} default)`);
3102
- output.writeln('');
3103
- for (const r of visible) {
3104
- const value = r.isOverride ? output.success(r.currentValue) : output.dim(`(default: ${r.defaultValue})`);
3105
- output.writeln(` ${r.name} = ${value}`);
3106
- output.writeln(` ${output.dim(`iter ${r.iter}`)} ${r.effect}`);
3107
- output.writeln('');
3108
- }
3109
- if (visible.length === 0 && onlyOverrides) {
3110
- output.writeln(output.dim(' No overrides — all router behavior is at defaults.'));
3111
- output.writeln('');
3112
- }
3113
- return { success: true, data: visible };
3114
- },
3115
- };
3116
- // ADR-149 iter 55 — side-by-side comparison of the two selector modes.
3117
- // Iter 29 added quality-best-under-budget; iter 30 added `decide` for the
3118
- // default cost-optimal mode. This subcommand runs BOTH on the same task
3119
- // so operators can see which mode is right for their workload BEFORE
3120
- // flipping CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK on for production.
3121
- const routerCompareModesCommand = {
3122
- name: 'compare-modes',
3123
- description: 'Compare selector modes side-by-side for a hypothetical task (cost-optimal vs cost-ceiling) — ADR-149 iter 55',
3124
- options: [
3125
- { name: 'task', short: 't', type: 'string', description: 'Task text (or positional arg)' },
3126
- { name: 'ceiling', type: 'number', description: 'Cost-ceiling $/Mtok for iter 29 mode (default 20)', default: '20' },
3127
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
3128
- ],
3129
- examples: [
3130
- { command: 'claude-flow neural router compare-modes "refactor strategy pattern"', description: 'Compare both modes for a task' },
3131
- { command: 'claude-flow neural router compare-modes -t "..." --ceiling 5', description: 'Cost-ceiling at $5 blended' },
3132
- ],
3133
- action: async (ctx) => {
3134
- const task = ctx.flags.task ?? (ctx.args && ctx.args[0]) ?? null;
3135
- if (!task) {
3136
- output.printError('Provide a task: --task "..." or as positional arg');
3137
- return { success: false, exitCode: 1 };
3138
- }
3139
- const ceiling = parseFloat(ctx.flags.ceiling || '20') || 20;
3140
- const fmt = ctx.flags.format || 'table';
3141
- const { embedTaskWithCache } = await import('../ruvector/task-embedder.js');
3142
- const { tryCostOptimalRoute, __resetNeuralRouterForTests } = await import('../ruvector/neural-router.js');
3143
- const { analyzeTaskComplexity } = await import('../ruvector/model-router.js');
3144
- const complexity = analyzeTaskComplexity(task);
3145
- const bucket = complexity.score < 0.34 ? 'low' : complexity.score < 0.67 ? 'med' : 'high';
3146
- let embedding;
3147
- try {
3148
- embedding = await embedTaskWithCache(task);
3149
- }
3150
- catch { /* */ }
3151
- if (!embedding) {
3152
- output.printError('Embedder unavailable — cannot run neural routing for comparison');
3153
- return { success: false, exitCode: 1 };
3154
- }
3155
- // Mode 1: cost-optimal (default — no ceiling)
3156
- process.env.CLAUDE_FLOW_ROUTER_NEURAL = '1';
3157
- delete process.env.CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK;
3158
- __resetNeuralRouterForTests();
3159
- const costOptimal = await tryCostOptimalRoute(embedding, { complexityBucket: bucket });
3160
- // Mode 2: cost-ceiling
3161
- process.env.CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK = String(ceiling);
3162
- __resetNeuralRouterForTests();
3163
- const costCeiling = await tryCostOptimalRoute(embedding, { complexityBucket: bucket });
3164
- // Clean up
3165
- delete process.env.CLAUDE_FLOW_ROUTER_COST_CEILING_USD_PER_MTOK;
3166
- __resetNeuralRouterForTests();
3167
- const payload = {
3168
- task: task.length > 200 ? task.slice(0, 200) + '…' : task,
3169
- complexity: complexity.score,
3170
- bucket,
3171
- modes: {
3172
- costOptimal: costOptimal ? {
3173
- modelId: costOptimal.modelId,
3174
- predictedQuality: costOptimal.predictedQuality,
3175
- costPerMTok: costOptimal.alternatives.find(a => a.modelId === costOptimal.modelId)?.costPerMTok ?? null,
3176
- metBar: costOptimal.metBar,
3177
- } : null,
3178
- costCeiling: costCeiling ? {
3179
- ceilingUsd: ceiling,
3180
- modelId: costCeiling.modelId,
3181
- predictedQuality: costCeiling.predictedQuality,
3182
- costPerMTok: costCeiling.alternatives.find(a => a.modelId === costCeiling.modelId)?.costPerMTok ?? null,
3183
- metBar: costCeiling.metBar,
3184
- } : null,
3185
- },
3186
- sameModel: costOptimal?.modelId === costCeiling?.modelId,
3187
- deltaQuality: (costCeiling && costOptimal) ? costCeiling.predictedQuality - costOptimal.predictedQuality : null,
3188
- deltaCost: (costCeiling && costOptimal)
3189
- ? ((costCeiling.alternatives.find(a => a.modelId === costCeiling.modelId)?.costPerMTok ?? 0) - (costOptimal.alternatives.find(a => a.modelId === costOptimal.modelId)?.costPerMTok ?? 0))
3190
- : null,
3191
- };
3192
- if (fmt === 'json') {
3193
- output.writeln(JSON.stringify(payload, null, 2));
3194
- return { success: true, data: payload };
3195
- }
3196
- output.writeln();
3197
- output.writeln(output.bold('Selector mode comparison (ADR-149 iter 55)'));
3198
- output.writeln(output.dim('─'.repeat(72)));
3199
- output.writeln(` Task: "${payload.task}"`);
3200
- output.writeln(` Complexity: ${complexity.score.toFixed(3)} (bucket: ${bucket})`);
3201
- output.writeln('');
3202
- output.writeln(output.bold(' Cost-optimal mode (default — cheapest above qualityBar):'));
3203
- if (costOptimal) {
3204
- const co = payload.modes.costOptimal;
3205
- output.writeln(` picked: ${output.success(co.modelId)}`);
3206
- output.writeln(` predicted Q: ${co.predictedQuality.toFixed(4)}`);
3207
- output.writeln(` cost ($/Mtok): $${co.costPerMTok?.toFixed(2)}`);
3208
- output.writeln(` met quality bar: ${co.metBar ? '✓' : '✗'}`);
3209
- }
3210
- else {
3211
- output.writeln(` ${output.warning('null (neural backend declined — would fall back to bandit)')}`);
3212
- }
3213
- output.writeln('');
3214
- output.writeln(output.bold(` Cost-ceiling mode (iter 29 — best quality ≤ $${ceiling}/Mtok):`));
3215
- if (costCeiling) {
3216
- const cc = payload.modes.costCeiling;
3217
- output.writeln(` picked: ${output.success(cc.modelId)}`);
3218
- output.writeln(` predicted Q: ${cc.predictedQuality.toFixed(4)}`);
3219
- output.writeln(` cost ($/Mtok): $${cc.costPerMTok?.toFixed(2)}`);
3220
- output.writeln(` met quality bar: ${cc.metBar ? '✓' : '✗'}`);
3221
- }
3222
- else {
3223
- output.writeln(` ${output.warning('null')}`);
3224
- }
3225
- output.writeln('');
3226
- if (costOptimal && costCeiling) {
3227
- if (payload.sameModel) {
3228
- output.writeln(output.dim(' Both modes picked the same model — selector choice irrelevant for this task.'));
3229
- }
3230
- else {
3231
- const dq = payload.deltaQuality;
3232
- const dc = payload.deltaCost;
3233
- const qSign = dq > 0 ? '+' : '';
3234
- const cSign = dc > 0 ? '+' : '';
3235
- const qColor = dq > 0 ? output.success(`${qSign}${dq.toFixed(4)}`) : output.warning(`${dq.toFixed(4)}`);
3236
- const cColor = dc > 0 ? output.warning(`${cSign}$${dc.toFixed(2)}`) : output.success(`$${dc.toFixed(2)}`);
3237
- output.writeln(` Δ (ceiling − optimal): predicted Q: ${qColor} cost: ${cColor}`);
3238
- output.writeln('');
3239
- output.writeln(output.dim(` Cost-ceiling pays extra cost for higher quality (or is forced cheap if ceiling is tight).`));
3240
- output.writeln(output.dim(` Cost-optimal accepts qualityBar threshold but minimizes spend.`));
3241
- }
3242
- }
3243
- output.writeln('');
3244
- return { success: true, data: payload };
3245
- },
3246
- };
3247
- // ADR-149 iter 49 — single-command SRE dashboard. The router has 13 subcommands
3248
- // (iter 48); ops want ONE that says "is everything working AND saving money?".
3249
- // Aggregates the most-asked signals into one terse screen.
3250
- const routerStatsSummaryCommand = {
3251
- name: 'stats-summary',
3252
- description: 'One-screen SRE dashboard: gate, recent activity, savings, bandit warmest cell (ADR-149 iter 49)',
3253
- options: [
3254
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
3255
- ],
3256
- examples: [
3257
- { command: 'claude-flow neural router stats-summary', description: 'One-screen health check' },
3258
- { command: 'claude-flow neural router stats-summary --format json | jq .', description: 'Pipe to dashboards / alerting' },
3259
- ],
3260
- action: async (ctx) => {
3261
- const fs = await import('node:fs');
3262
- const path = await import('node:path');
3263
- const fmt = ctx.flags.format || 'table';
3264
- // 1. Backend gate / status
3265
- const { neuralRouterStatus } = await import('../ruvector/neural-router.js');
3266
- const { getModelRouterStats } = await import('../ruvector/model-router.js');
3267
- const backend = await neuralRouterStatus();
3268
- const stats = getModelRouterStats();
3269
- // 2. Trajectory existence + basic counts
3270
- const trajectoryPath = process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH
3271
- ?? path.resolve(process.cwd(), '.swarm', 'model-router-trajectories.jsonl');
3272
- let trajectory = { exists: false, rows: 0, decisions: 0, outcomes: 0, pairedWithCost: 0, oldestTs: null, newestTs: null };
3273
- let recent24h = null;
3274
- let cost7d = null;
3275
- if (fs.existsSync(trajectoryPath)) {
3276
- trajectory.exists = true;
3277
- const { MODEL_PRICES } = await import('../ruvector/model-prices.js');
3278
- const lines = fs.readFileSync(trajectoryPath, 'utf8').split('\n').filter(l => l.trim().length > 0);
3279
- trajectory.rows = lines.length;
3280
- const cutoff24h = Date.now() - 24 * 3600_000;
3281
- const cutoff7d = Date.now() - 7 * 86400_000;
3282
- let recDec = 0, recFallback = 0;
3283
- const decs7d = new Map();
3284
- const outs7d = new Map();
3285
- for (const l of lines) {
3286
- try {
3287
- const r = JSON.parse(l);
3288
- const tsMs = Date.parse(r.ts);
3289
- if (r.type === 'decision') {
3290
- trajectory.decisions++;
3291
- if (tsMs >= cutoff24h) {
3292
- recDec++;
3293
- if (r.routed_by === 'bandit-fallback')
3294
- recFallback++;
3295
- }
3296
- if (tsMs >= cutoff7d)
3297
- decs7d.set(r.task_hash, r);
3298
- }
3299
- else if (r.type === 'outcome') {
3300
- trajectory.outcomes++;
3301
- if (r.cost_usd != null)
3302
- trajectory.pairedWithCost++;
3303
- if (tsMs >= cutoff7d)
3304
- outs7d.set(r.task_hash, r);
3305
- }
3306
- if (r.ts) {
3307
- if (!trajectory.oldestTs || r.ts < trajectory.oldestTs)
3308
- trajectory.oldestTs = r.ts;
3309
- if (!trajectory.newestTs || r.ts > trajectory.newestTs)
3310
- trajectory.newestTs = r.ts;
3311
- }
3312
- }
3313
- catch { /* malformed */ }
3314
- }
3315
- recent24h = {
3316
- decisions: recDec, fallbacks: recFallback,
3317
- fallbackRatePct: recDec > 0 ? Math.round((recFallback / recDec) * 10000) / 100 : 0,
3318
- };
3319
- // 7-day cost via heuristic counterfactual
3320
- let pairs = 0, actual = 0, cf = 0;
3321
- for (const [hash, dec] of decs7d) {
3322
- const out = outs7d.get(hash);
3323
- if (!out?.cost_usd || !out.tokens)
3324
- continue;
3325
- pairs++;
3326
- actual += out.cost_usd;
3327
- const tierModel = dec.complexity < 0.34 ? 'haiku' : dec.complexity < 0.67 ? 'sonnet' : 'opus';
3328
- const cfModel = dec.ab_pair?.bandit_pick ?? tierModel;
3329
- const p = MODEL_PRICES[cfModel] ?? { in: 1, out: 1 };
3330
- cf += (out.tokens.input * p.in + out.tokens.output * p.out) / 1_000_000;
3331
- }
3332
- if (pairs > 0) {
3333
- cost7d = {
3334
- pairs,
3335
- actualUsd: Math.round(actual * 1_000_000) / 1_000_000,
3336
- counterfactualUsd: Math.round(cf * 1_000_000) / 1_000_000,
3337
- savingsUsd: Math.round((cf - actual) * 1_000_000) / 1_000_000,
3338
- savingsPct: cf > 0 ? Math.round(((cf - actual) / cf) * 10000) / 100 : 0,
3339
- };
3340
- }
3341
- }
3342
- let warmestCell = null;
3343
- const statePath = path.resolve(process.cwd(), '.swarm', 'model-router-state.json');
3344
- if (fs.existsSync(statePath)) {
3345
- try {
3346
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
3347
- const priors = state.priorsById ?? state.priors ?? {};
3348
- let bestSamples = 0;
3349
- for (const bucket of ['low', 'med', 'high']) {
3350
- const b = priors[bucket];
3351
- if (!b)
3352
- continue;
3353
- for (const [k, p] of Object.entries(b)) {
3354
- const samples = p.alpha + p.beta - 2;
3355
- if (samples > bestSamples) {
3356
- bestSamples = samples;
3357
- warmestCell = { bucket, key: k, samples, meanQuality: p.alpha / (p.alpha + p.beta) };
3358
- }
3359
- }
3360
- }
3361
- }
3362
- catch { /* malformed */ }
3363
- }
3364
- const payload = {
3365
- backend: {
3366
- enabled: backend.enabled,
3367
- available: backend.available,
3368
- routedBy: backend.routedBy,
3369
- reason: backend.reason,
3370
- },
3371
- processLocal: {
3372
- totalDecisions: stats.totalDecisions,
3373
- modelDistribution: stats.modelDistribution,
3374
- routedByCounts: stats.routedByCounts,
3375
- abDisagreementRate: stats.ab.comparisons > 0 ? Math.round(stats.ab.disagreementRate * 10000) / 100 : 0,
3376
- },
3377
- trajectory,
3378
- recent24h,
3379
- cost7d,
3380
- warmestBanditCell: warmestCell,
3381
- };
3382
- if (fmt === 'json') {
3383
- output.writeln(JSON.stringify(payload, null, 2));
3384
- return { success: true, data: payload };
3385
- }
3386
- output.writeln();
3387
- output.writeln(output.bold('Router stats summary — ADR-149 iter 49 (one-screen SRE view)'));
3388
- output.writeln(output.dim('─'.repeat(72)));
3389
- output.writeln('');
3390
- output.writeln(output.bold(' Backend:'));
3391
- output.writeln(` gate: ${backend.enabled ? output.success('open (NEURAL=1)') : output.warning('closed')}`);
3392
- output.writeln(` available: ${backend.available ? output.success('yes') : output.warning('no')}`);
3393
- output.writeln(` active backend: ${backend.routedBy ?? '—'}`);
3394
- output.writeln(` reason: ${backend.reason}`);
3395
- output.writeln('');
3396
- output.writeln(output.bold(' Process-local (since this server started):'));
3397
- output.writeln(` decisions: ${stats.totalDecisions}`);
3398
- output.writeln(` routed_by: heuristic=${stats.routedByCounts.heuristic} hybrid=${stats.routedByCounts.hybrid} bandit-fallback=${stats.routedByCounts['bandit-fallback']}`);
3399
- if (stats.ab.comparisons > 0) {
3400
- output.writeln(` A/B: ${stats.ab.comparisons} comparisons, ${stats.ab.disagreements} disagreements (${(stats.ab.disagreementRate * 100).toFixed(1)}%)`);
3401
- }
3402
- output.writeln('');
3403
- output.writeln(output.bold(' Trajectory log:'));
3404
- if (!trajectory.exists) {
3405
- output.writeln(` ${output.warning('file does not exist — recorder OFF or no decisions made')}`);
3406
- }
3407
- else {
3408
- output.writeln(` rows: ${trajectory.rows} (${trajectory.decisions} decisions / ${trajectory.outcomes} outcomes / ${trajectory.pairedWithCost} cost-bearing)`);
3409
- if (trajectory.oldestTs && trajectory.newestTs) {
3410
- output.writeln(` span: ${trajectory.oldestTs.slice(0, 19)} → ${trajectory.newestTs.slice(0, 19)}`);
3411
- }
3412
- }
3413
- output.writeln('');
3414
- if (recent24h) {
3415
- const rateStr = recent24h.fallbackRatePct > 30 ? output.warning(recent24h.fallbackRatePct + '% ⚠')
3416
- : recent24h.fallbackRatePct > 10 ? recent24h.fallbackRatePct + '%'
3417
- : output.success(recent24h.fallbackRatePct + '%');
3418
- output.writeln(output.bold(` Last 24h:`));
3419
- output.writeln(` decisions: ${recent24h.decisions}`);
3420
- output.writeln(` fallback rate: ${rateStr} (neural backend → bandit when prediction unusable)`);
3421
- output.writeln('');
3422
- }
3423
- if (cost7d) {
3424
- const savingsStr = cost7d.savingsUsd >= 0
3425
- ? output.success(`$${cost7d.savingsUsd.toFixed(4)}`)
3426
- : output.warning(`-$${Math.abs(cost7d.savingsUsd).toFixed(4)}`);
3427
- output.writeln(output.bold(' Last 7d cost-savings (vs heuristic baseline):'));
3428
- output.writeln(` paired calls: ${cost7d.pairs}`);
3429
- output.writeln(` actual: $${cost7d.actualUsd.toFixed(4)}`);
3430
- output.writeln(` counterfactual: $${cost7d.counterfactualUsd.toFixed(4)}`);
3431
- output.writeln(` savings: ${savingsStr} (${cost7d.savingsPct}%)`);
3432
- output.writeln('');
3433
- }
3434
- if (warmestCell) {
3435
- output.writeln(output.bold(' Bandit warmest cell:'));
3436
- output.writeln(` ${warmestCell.bucket} × ${warmestCell.key} → ${warmestCell.samples} samples, meanQ=${warmestCell.meanQuality.toFixed(3)}`);
3437
- output.writeln('');
3438
- }
3439
- output.writeln(output.dim(' For drill-down: `router decisions`, `router cost-savings`, `router bandit-state`'));
3440
- output.writeln('');
3441
- return { success: true, data: payload };
3442
- },
3443
- };
3444
- // ADR-149 iter 48 — bandit-state inspection. The persisted bandit posteriors
3445
- // (`.swarm/model-router-state.json`) accumulate across restarts but are
3446
- // otherwise invisible. Surfacing the (bucket × model) prior matrix lets
3447
- // operators see where bandit learning is thin (cold cells) and where it's
3448
- // confident (large α+β).
3449
- const routerBanditStateCommand = {
3450
- name: 'bandit-state',
3451
- description: 'Inspect persisted bandit Beta priors per bucket × model (ADR-149 iter 48)',
3452
- options: [
3453
- { name: 'path', type: 'string', description: 'Path to model-router-state.json (default: .swarm/model-router-state.json)' },
3454
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
3455
- { name: 'cold-threshold', type: 'number', description: 'Highlight cells with sample count below this (default 4 — matches iter 14 density guard)', default: '4' },
3456
- ],
3457
- examples: [
3458
- { command: 'claude-flow neural router bandit-state', description: 'Show all Beta priors per bucket × tier + per bucket × modelId' },
3459
- { command: 'claude-flow neural router bandit-state --format json | jq .priorsById', description: 'Just the per-modelId matrix' },
3460
- ],
3461
- action: async (ctx) => {
3462
- const fs = await import('node:fs');
3463
- const path = await import('node:path');
3464
- const statePath = ctx.flags.path
3465
- ?? path.resolve(process.cwd(), '.swarm', 'model-router-state.json');
3466
- const fmt = ctx.flags.format || 'table';
3467
- const coldThreshold = parseInt(ctx.flags['cold-threshold'] || '4', 10) || 4;
3468
- if (!fs.existsSync(statePath)) {
3469
- const msg = `Bandit state file not found at ${statePath}`;
3470
- if (fmt === 'json')
3471
- output.writeln(JSON.stringify({ error: msg, hint: 'State is created on first routing decision. Run any agent_spawn flow with CLAUDE_FLOW_ROUTER_NEURAL=1.' }, null, 2));
3472
- else {
3473
- output.printError(msg);
3474
- output.writeln(output.dim(' State is created on first routing decision. Run any agent_spawn flow.'));
3475
- }
3476
- return { success: false, exitCode: 1 };
3477
- }
3478
- let state;
3479
- try {
3480
- state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
3481
- }
3482
- catch (err) {
3483
- const msg = `Failed to parse bandit state: ${err instanceof Error ? err.message : String(err)}`;
3484
- if (fmt === 'json')
3485
- output.writeln(JSON.stringify({ error: msg }, null, 2));
3486
- else
3487
- output.printError(msg);
3488
- return { success: false, exitCode: 1 };
3489
- }
3490
- // Helper: enumerate all cells from a BucketedPriors with derived stats.
3491
- const cells = (priors) => {
3492
- const out = [];
3493
- if (!priors)
3494
- return out;
3495
- for (const bucket of ['low', 'med', 'high']) {
3496
- const b = priors[bucket];
3497
- if (!b)
3498
- continue;
3499
- for (const [k, p] of Object.entries(b)) {
3500
- const samples = p.alpha + p.beta - 2; // -2 because Beta(1,1) is the uniform prior
3501
- const meanQuality = p.alpha / (p.alpha + p.beta);
3502
- out.push({ bucket, key: k, alpha: p.alpha, beta: p.beta, samples, meanQuality, cold: samples < coldThreshold });
3503
- }
3504
- }
3505
- return out;
3506
- };
3507
- const tierCells = cells(state.priors);
3508
- const idCells = cells(state.priorsById);
3509
- const coldTierCells = tierCells.filter(c => c.cold);
3510
- const coldIdCells = idCells.filter(c => c.cold);
3511
- const payload = {
3512
- input: statePath,
3513
- stateVersion: state.version ?? 2,
3514
- totalDecisions: state.totalDecisions ?? 0,
3515
- lastUpdated: state.lastUpdated ?? null,
3516
- coldThreshold,
3517
- priors: tierCells,
3518
- priorsById: idCells,
3519
- summary: {
3520
- tierCells: tierCells.length,
3521
- coldTierCells: coldTierCells.length,
3522
- idCells: idCells.length,
3523
- coldIdCells: coldIdCells.length,
3524
- warmestIdCell: idCells.length > 0 ? [...idCells].sort((a, b) => b.samples - a.samples)[0] : null,
3525
- },
3526
- };
3527
- if (fmt === 'json') {
3528
- output.writeln(JSON.stringify(payload, null, 2));
3529
- return { success: true, data: payload };
3530
- }
3531
- output.writeln();
3532
- output.writeln(output.bold('Bandit state inspection (ADR-149 iter 48)'));
3533
- output.writeln(output.dim('─'.repeat(72)));
3534
- output.writeln(` Path: ${statePath}`);
3535
- output.writeln(` Schema version: v${state.version ?? 2} (v2=tier priors only; v3=adds priorsById)`);
3536
- output.writeln(` Total decisions: ${state.totalDecisions ?? 0}`);
3537
- if (state.lastUpdated)
3538
- output.writeln(` Last update: ${state.lastUpdated}`);
3539
- output.writeln(` Cold threshold: samples < ${coldThreshold} (iter 14 density-guard cutoff)`);
3540
- output.writeln('');
3541
- const renderTable = (label, rows) => {
3542
- if (rows.length === 0) {
3543
- output.writeln(` ${label}: (empty — no outcomes recorded yet for this layer)`);
3544
- output.writeln('');
3545
- return;
3546
- }
3547
- output.writeln(output.bold(` ${label}:`));
3548
- output.writeln(' bucket key α β samples meanQ');
3549
- for (const c of rows.sort((a, b) => (a.bucket.localeCompare(b.bucket)) || (b.samples - a.samples))) {
3550
- const coldMark = c.cold ? output.warning(' ❄ cold') : '';
3551
- output.writeln(` ${c.bucket.padEnd(6)} ${c.key.padEnd(38)} ${c.alpha.toFixed(1).padStart(5)} ${c.beta.toFixed(1).padStart(5)} ${String(c.samples).padStart(7)} ${c.meanQuality.toFixed(3)}${coldMark}`);
3552
- }
3553
- output.writeln('');
3554
- };
3555
- renderTable('Tier priors (bucket × tier label)', tierCells);
3556
- renderTable('Per-modelId priors (bucket × concrete modelId, iter 14)', idCells);
3557
- output.writeln(output.bold(' Summary:'));
3558
- output.writeln(` tier cells: ${tierCells.length} (cold: ${coldTierCells.length})`);
3559
- output.writeln(` per-modelId cells: ${idCells.length} (cold: ${coldIdCells.length})`);
3560
- if (payload.summary.warmestIdCell) {
3561
- const w = payload.summary.warmestIdCell;
3562
- output.writeln(` warmest cell: ${w.bucket} × ${w.key} (${w.samples} samples, meanQ=${w.meanQuality.toFixed(3)})`);
3563
- }
3564
- output.writeln('');
3565
- if (coldIdCells.length > 0 && idCells.length > 0) {
3566
- output.writeln(output.dim(` Cold cells suppress iter 14 per-modelId Thompson perturbation. Until α+β ≥ ${coldThreshold + 2},`));
3567
- output.writeln(output.dim(' the neural prediction dominates that (bucket, modelId) pair without bandit correction.'));
3568
- output.writeln('');
3569
- }
3570
- return { success: true, data: payload };
3571
- },
3572
- };
3573
- // ADR-149 iter 38 — consumer for iter 37's sampled A/B mode. Aggregates
3574
- // ab_pair from decision rows into a (bandit_pick × hybrid_pick) confusion
3575
- // matrix plus disagreement rate. Operators see WHERE the neural prior
3576
- // moves the bandit's decisions.
3577
- const routerAbStatsCommand = {
3578
- name: 'ab-stats',
3579
- description: 'Aggregate A/B (bandit-vs-hybrid) disagreement from trajectory ab_pair rows (ADR-149 iter 38)',
3580
- options: [
3581
- { name: 'in', short: 'i', type: 'string', description: 'Trajectory JSONL path (default: $CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH or .swarm/...)' },
3582
- { name: 'since', short: 's', type: 'string', description: 'Time window suffix: 1h, 24h, 7d, 30d' },
3583
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
3584
- ],
3585
- examples: [
3586
- { command: 'claude-flow neural router ab-stats', description: 'All A/B comparisons recorded so far' },
3587
- { command: 'claude-flow neural router ab-stats --since 7d --format json', description: 'Last 7 days, pipe-friendly' },
3588
- { command: 'CLAUDE_FLOW_ROUTER_AB_SAMPLE_RATE=0.05 ... && claude-flow neural router ab-stats', description: 'After running with iter 37 sampling on' },
3589
- ],
3590
- action: async (ctx) => {
3591
- const fs = await import('node:fs');
3592
- const path = await import('node:path');
3593
- const inPath = ctx.flags.in
3594
- ?? process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH
3595
- ?? path.resolve(process.cwd(), '.swarm', 'model-router-trajectories.jsonl');
3596
- const since = ctx.flags.since;
3597
- const fmt = ctx.flags.format || 'table';
3598
- if (!fs.existsSync(inPath)) {
3599
- const msg = `Trajectory file not found at ${inPath}`;
3600
- if (fmt === 'json')
3601
- output.writeln(JSON.stringify({ error: msg }, null, 2));
3602
- else {
3603
- output.printError(msg);
3604
- output.writeln(output.dim(' Set CLAUDE_FLOW_ROUTER_TRAJECTORY=1 + CLAUDE_FLOW_ROUTER_AB_SAMPLE_RATE=0.05 to start collecting ab_pair data.'));
3605
- }
3606
- return { success: false, exitCode: 1 };
3607
- }
3608
- // Parse + filter to decision rows that carry ab_pair.
3609
- let cutoffMs = null;
3610
- if (since) {
3611
- const m = since.match(/^(\d+)([hdmw])$/);
3612
- if (m) {
3613
- const n = parseInt(m[1], 10);
3614
- const unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000 }[m[2]] ?? 0;
3615
- cutoffMs = Date.now() - n * unitMs;
3616
- }
3617
- }
3618
- const rows = fs.readFileSync(inPath, 'utf8').split('\n').filter(l => l.trim().length > 0);
3619
- let totalDecisions = 0;
3620
- let malformed = 0;
3621
- const abRows = [];
3622
- for (const l of rows) {
3623
- try {
3624
- const r = JSON.parse(l);
3625
- if (r.type !== 'decision')
3626
- continue;
3627
- totalDecisions++;
3628
- if (!r.ab_pair)
3629
- continue;
3630
- if (cutoffMs !== null && Date.parse(r.ts) < cutoffMs)
3631
- continue;
3632
- abRows.push(r);
3633
- }
3634
- catch {
3635
- malformed++;
3636
- }
3637
- }
3638
- const cells = new Map();
3639
- let disagree = 0;
3640
- const banditTotals = {};
3641
- const hybridTotals = {};
3642
- for (const r of abRows) {
3643
- const ap = r.ab_pair;
3644
- const key = `${ap.bandit_pick}→${ap.hybrid_pick}`;
3645
- const cell = cells.get(key) ?? { bandit: ap.bandit_pick, hybrid: ap.hybrid_pick, count: 0 };
3646
- cell.count++;
3647
- cells.set(key, cell);
3648
- banditTotals[ap.bandit_pick] = (banditTotals[ap.bandit_pick] ?? 0) + 1;
3649
- hybridTotals[ap.hybrid_pick] = (hybridTotals[ap.hybrid_pick] ?? 0) + 1;
3650
- if (ap.disagree)
3651
- disagree++;
3652
- }
3653
- const disagreeRatePct = abRows.length > 0 ? Math.round((disagree / abRows.length) * 10000) / 100 : 0;
3654
- // Models actually seen, in stable iteration order.
3655
- const allModels = Array.from(new Set([...Object.keys(banditTotals), ...Object.keys(hybridTotals)])).sort();
3656
- const matrix = {};
3657
- for (const b of allModels) {
3658
- matrix[b] = {};
3659
- for (const h of allModels) {
3660
- matrix[b][h] = cells.get(`${b}→${h}`)?.count ?? 0;
3661
- }
3662
- }
3663
- // Disagreement breakdown — off-diagonal cells sorted by count desc.
3664
- const offDiag = [];
3665
- for (const c of cells.values()) {
3666
- if (c.bandit !== c.hybrid) {
3667
- offDiag.push({ ...c, pctOfDisagrees: disagree > 0 ? Math.round((c.count / disagree) * 10000) / 100 : 0 });
3668
- }
3669
- }
3670
- offDiag.sort((a, b) => b.count - a.count);
3671
- const payload = {
3672
- input: inPath,
3673
- filters: { since },
3674
- totalDecisions, malformed,
3675
- abComparisons: abRows.length,
3676
- disagreements: disagree,
3677
- disagreementRatePct: disagreeRatePct,
3678
- models: allModels,
3679
- banditTotals, hybridTotals,
3680
- confusionMatrix: matrix,
3681
- disagreementBreakdown: offDiag,
3682
- coveragePct: totalDecisions > 0 ? Math.round((abRows.length / totalDecisions) * 10000) / 100 : 0,
3683
- };
3684
- if (fmt === 'json') {
3685
- output.writeln(JSON.stringify(payload, null, 2));
3686
- return { success: true, data: payload };
3687
- }
3688
- output.writeln();
3689
- output.writeln(output.bold('A/B (bandit vs hybrid) stats — ADR-149 iter 37/38'));
3690
- output.writeln(output.dim('─'.repeat(72)));
3691
- output.writeln(` Input: ${inPath}`);
3692
- if (since)
3693
- output.writeln(` Time window: since ${since}`);
3694
- output.writeln(` Total decisions: ${totalDecisions} (${malformed} malformed)`);
3695
- output.writeln(` A/B comparisons: ${abRows.length} (${payload.coveragePct}% of decisions had ab_pair)`);
3696
- output.writeln('');
3697
- if (abRows.length === 0) {
3698
- output.writeln(output.dim(' No ab_pair rows found. Enable iter 37 sampling:'));
3699
- output.writeln(output.dim(' export CLAUDE_FLOW_ROUTER_TRAJECTORY=1'));
3700
- output.writeln(output.dim(' export CLAUDE_FLOW_ROUTER_AB_SAMPLE_RATE=0.05 # 5% sampling'));
3701
- output.writeln('');
3702
- return { success: true, data: payload };
3703
- }
3704
- output.writeln(` Disagreements: ${disagree} (${disagreeRatePct}% of A/B comparisons)`);
3705
- output.writeln('');
3706
- output.writeln(output.bold(` Confusion matrix (rows = bandit pick, cols = hybrid pick, ${allModels.length} models):`));
3707
- const headerPad = Math.max(8, ...allModels.map(m => m.length));
3708
- output.writeln(' ' + 'bandit \\ hybrid'.padEnd(headerPad) + ' ' + allModels.map(m => m.padStart(8)).join(''));
3709
- for (const b of allModels) {
3710
- const cells = allModels.map(h => String(matrix[b][h] ?? 0).padStart(8));
3711
- output.writeln(` ${b.padEnd(headerPad)} ${cells.join('')}`);
3712
- }
3713
- output.writeln(output.dim(' (Diagonal cells = agreement, off-diagonal = disagreement.)'));
3714
- output.writeln('');
3715
- if (offDiag.length > 0) {
3716
- output.writeln(output.bold(' Disagreement breakdown (bandit → hybrid):'));
3717
- output.writeln(' transition'.padEnd(40) + ' count % of disagrees');
3718
- for (const c of offDiag) {
3719
- output.writeln(` ${(c.bandit + ' → ' + c.hybrid).padEnd(38)} ${String(c.count).padStart(5)} ${c.pctOfDisagrees.toString().padStart(6)}%`);
3720
- }
3721
- output.writeln('');
3722
- }
3723
- return { success: true, data: payload };
3724
- },
3725
- };
3726
- // ADR-149 iter 43 — show the canonical price table that drives cost
3727
- // computations, blended-price routing, and counterfactual baselines.
3728
- // Operators ask "what does the router think gpt-4.1 costs?" frequently;
3729
- // previously the answer required reading src/ruvector/model-prices.ts.
3730
- const routerPricesCommand = {
3731
- name: 'prices',
3732
- description: 'Show the per-model price table that drives blended cost + counterfactual computations (ADR-149 iter 43)',
3733
- options: [
3734
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
3735
- { name: 'sort', type: 'string', description: 'Sort by: blended (default), input, output, name', default: 'blended' },
3736
- ],
3737
- examples: [
3738
- { command: 'claude-flow neural router prices', description: 'Show all models sorted by blended price' },
3739
- { command: 'claude-flow neural router prices --sort name', description: 'Sort alphabetically' },
3740
- { command: 'claude-flow neural router prices --format json | jq \'.[] | select(.id | contains("opus"))\'', description: 'Filter via jq' },
3741
- ],
3742
- action: async (ctx) => {
3743
- const { MODEL_PRICES, blendedPrice } = await import('../ruvector/model-prices.js');
3744
- const fmt = ctx.flags.format || 'table';
3745
- const sortKey = (ctx.flags.sort || 'blended').toLowerCase();
3746
- const rows = Object.entries(MODEL_PRICES).map(([id, p]) => ({
3747
- id,
3748
- inPerMtok: p.in,
3749
- outPerMtok: p.out,
3750
- blendedPerMtok: blendedPrice(id),
3751
- }));
3752
- switch (sortKey) {
3753
- case 'input':
3754
- rows.sort((a, b) => a.inPerMtok - b.inPerMtok);
3755
- break;
3756
- case 'output':
3757
- rows.sort((a, b) => a.outPerMtok - b.outPerMtok);
3758
- break;
3759
- case 'name':
3760
- rows.sort((a, b) => a.id.localeCompare(b.id));
3761
- break;
3762
- case 'blended':
3763
- default: rows.sort((a, b) => a.blendedPerMtok - b.blendedPerMtok);
3764
- }
3765
- if (fmt === 'json') {
3766
- output.writeln(JSON.stringify(rows, null, 2));
3767
- return { success: true, data: rows };
3768
- }
3769
- output.writeln();
3770
- output.writeln(output.bold('Model price table (ADR-149 iter 31/43 — single source of truth)'));
3771
- output.writeln(output.dim('─'.repeat(72)));
3772
- output.writeln(` ${rows.length} entries, sorted by ${sortKey}`);
3773
- output.writeln('');
3774
- output.writeln(' model id $/Mtok in $/Mtok out blended ($/Mtok)');
3775
- for (const r of rows) {
3776
- output.writeln(` ${r.id.padEnd(44)} ${('$' + r.inPerMtok.toFixed(2)).padStart(9)} ${('$' + r.outPerMtok.toFixed(2)).padStart(11)} ${('$' + r.blendedPerMtok.toFixed(2)).padStart(15)}`);
3777
- }
3778
- output.writeln('');
3779
- output.writeln(output.dim(' Blended = $/Mtok_in + 3 × $/Mtok_out (1 input : 3 output ratio for code tasks).'));
3780
- output.writeln(output.dim(' Unknown model ids fall back to $1/Mtok blended (1×in + 1×out).'));
3781
- output.writeln('');
3782
- return { success: true, data: rows };
3783
- },
3784
- };
3785
- // ADR-149 iter 41 — forward-looking budget projection. Iter 32-34 measure
3786
- // past cost (actual vs counterfactual, per-window drift). This subcommand
3787
- // extrapolates: given the measured rate and average cost per decision over
3788
- // a recent window, what will routing cost over the next 7d / 30d / 90d /
3789
- // 365d? Also projects the heuristic baseline so operators see the savings
3790
- // trajectory across a quarter or year.
3791
- const routerCostProjectionCommand = {
3792
- name: 'cost-projection',
3793
- description: 'Project monthly/quarterly cost from measured rate (ADR-149 iter 41)',
3794
- options: [
3795
- { name: 'in', short: 'i', type: 'string', description: 'Trajectory JSONL path (default: $CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH or .swarm/...)' },
3796
- { name: 'window', short: 'w', type: 'string', description: 'Measurement window to extrapolate FROM (default 7d). Format: 1h, 24h, 7d, 30d' },
3797
- { name: 'horizons', type: 'string', description: 'Projection horizons (CSV of duration suffixes). Default: 7d,30d,90d,365d' },
3798
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
3799
- ],
3800
- examples: [
3801
- { command: 'claude-flow neural router cost-projection', description: 'Project from last 7d of data → 7d/30d/90d/365d horizons' },
3802
- { command: 'claude-flow neural router cost-projection --window 24h --horizons 7d,30d', description: 'Project from last day only' },
3803
- { command: 'claude-flow neural router cost-projection --format json | jq .horizons[1].projectedSavingsUsd', description: '30-day savings projection for dashboards' },
3804
- ],
3805
- action: async (ctx) => {
3806
- const fs = await import('node:fs');
3807
- const path = await import('node:path');
3808
- const { MODEL_PRICES } = await import('../ruvector/model-prices.js');
3809
- const inPath = ctx.flags.in
3810
- ?? process.env.CLAUDE_FLOW_ROUTER_TRAJECTORY_PATH
3811
- ?? path.resolve(process.cwd(), '.swarm', 'model-router-trajectories.jsonl');
3812
- const windowSpec = ctx.flags.window ?? '7d';
3813
- const horizonSpecs = (ctx.flags.horizons ?? '7d,30d,90d,365d').split(',').map(s => s.trim());
3814
- const fmt = ctx.flags.format || 'table';
3815
- const parseDuration = (s) => {
3816
- const m = s.match(/^(\d+)([hdmw])$/);
3817
- if (!m)
3818
- return null;
3819
- const n = parseInt(m[1], 10);
3820
- const unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000 }[m[2]] ?? 0;
3821
- return n * unitMs;
3822
- };
3823
- const windowMs = parseDuration(windowSpec);
3824
- if (!windowMs || windowMs <= 0) {
3825
- output.printError(`--window must be Nh/Nd/Nm/Nw (got ${windowSpec})`);
3826
- return { success: false, exitCode: 1 };
3827
- }
3828
- if (!fs.existsSync(inPath)) {
3829
- const msg = `Trajectory file not found at ${inPath}`;
3830
- if (fmt === 'json')
3831
- output.writeln(JSON.stringify({ error: msg }, null, 2));
3832
- else
3833
- output.printError(msg);
3834
- return { success: false, exitCode: 1 };
3835
- }
3836
- // iter 66 — outcomes now ARRAY (was Map). Same fix as iter 62/63/65:
3837
- // a Map keyed by task_hash collapses multiple runs of the same task to
3838
- // the LATEST outcome only, biasing the rate downward (pairCount =
3839
- // unique-tasks, not unique-calls). Production projections were
3840
- // systematically too small for any workload with recurring tasks.
3841
- const decisions = new Map();
3842
- const outcomes = [];
3843
- let malformed = 0;
3844
- const cutoffMs = Date.now() - windowMs;
3845
- for (const l of fs.readFileSync(inPath, 'utf8').split('\n')) {
3846
- if (!l.trim())
3847
- continue;
3848
- try {
3849
- const r = JSON.parse(l);
3850
- if (Date.parse(r.ts) < cutoffMs)
3851
- continue;
3852
- if (r.type === 'decision')
3853
- decisions.set(r.task_hash, r);
3854
- else if (r.type === 'outcome')
3855
- outcomes.push(r);
3856
- }
3857
- catch {
3858
- malformed++;
3859
- }
3860
- }
3861
- // Pair + compute window totals. Iterate OUTCOMES (not deduped decisions);
3862
- // each outcome row contributes its own cost + tokens to the rate.
3863
- let pairCount = 0;
3864
- let actualUsd = 0;
3865
- let counterfactualUsd = 0; // heuristic = tier-by-complexity baseline (iter 32 default)
3866
- let totalInputTokens = 0;
3867
- let totalOutputTokens = 0;
3868
- for (const out of outcomes) {
3869
- if (!out?.cost_usd || !out.tokens)
3870
- continue;
3871
- const dec = decisions.get(out.task_hash);
3872
- if (!dec)
3873
- continue;
3874
- pairCount++;
3875
- actualUsd += out.cost_usd;
3876
- totalInputTokens += out.tokens.input;
3877
- totalOutputTokens += out.tokens.output;
3878
- // Heuristic counterfactual: tier-by-complexity (matches iter 32 default).
3879
- const tierModel = dec.complexity < 0.34 ? 'haiku'
3880
- : dec.complexity < 0.67 ? 'sonnet' : 'opus';
3881
- const cfModel = dec.ab_pair?.bandit_pick ?? tierModel;
3882
- const p = MODEL_PRICES[cfModel] ?? { in: 1, out: 1 };
3883
- counterfactualUsd += (out.tokens.input * p.in + out.tokens.output * p.out) / 1_000_000;
3884
- }
3885
- if (pairCount === 0) {
3886
- const msg = `No paired cost-bearing rows in the last ${windowSpec}. Cannot project.`;
3887
- const payload = { error: msg, windowSpec, hint: 'Enable CLAUDE_FLOW_ROUTER_TRAJECTORY=1 and run some routed agent calls first.' };
3888
- if (fmt === 'json')
3889
- output.writeln(JSON.stringify(payload, null, 2));
3890
- else {
3891
- output.printError(msg);
3892
- output.writeln(output.dim(` ${payload.hint}`));
3893
- }
3894
- return { success: true, data: payload };
3895
- }
3896
- // Per-second rate from this window's pair count.
3897
- const callsPerSecond = pairCount / (windowMs / 1000);
3898
- const avgActualPerCall = actualUsd / pairCount;
3899
- const avgCounterfactualPerCall = counterfactualUsd / pairCount;
3900
- // Extrapolate to horizons. Costs scale linearly with calls; calls scale
3901
- // linearly with elapsed time at the measured rate.
3902
- const horizons = horizonSpecs.map(spec => {
3903
- const ms = parseDuration(spec);
3904
- if (!ms)
3905
- return { spec, error: 'invalid duration' };
3906
- const projectedCalls = Math.round(callsPerSecond * (ms / 1000));
3907
- const projectedActualUsd = avgActualPerCall * projectedCalls;
3908
- const projectedCounterfactualUsd = avgCounterfactualPerCall * projectedCalls;
3909
- const projectedSavingsUsd = projectedCounterfactualUsd - projectedActualUsd;
3910
- const projectedSavingsPct = projectedCounterfactualUsd > 0
3911
- ? (projectedSavingsUsd / projectedCounterfactualUsd) * 100 : 0;
3912
- return {
3913
- spec, durationMs: ms,
3914
- projectedCalls,
3915
- projectedActualUsd: Math.round(projectedActualUsd * 1_000_000) / 1_000_000,
3916
- projectedCounterfactualUsd: Math.round(projectedCounterfactualUsd * 1_000_000) / 1_000_000,
3917
- projectedSavingsUsd: Math.round(projectedSavingsUsd * 1_000_000) / 1_000_000,
3918
- projectedSavingsPct: Math.round(projectedSavingsPct * 100) / 100,
3919
- };
3920
- });
3921
- const payload = {
3922
- input: inPath,
3923
- window: windowSpec,
3924
- malformed,
3925
- measurement: {
3926
- pairs: pairCount,
3927
- actualUsd: Math.round(actualUsd * 1_000_000) / 1_000_000,
3928
- counterfactualUsd: Math.round(counterfactualUsd * 1_000_000) / 1_000_000,
3929
- savingsUsd: Math.round((counterfactualUsd - actualUsd) * 1_000_000) / 1_000_000,
3930
- callsPerSecond: Math.round(callsPerSecond * 1_000_000) / 1_000_000,
3931
- callsPerDay: Math.round(callsPerSecond * 86400 * 100) / 100,
3932
- avgActualPerCall: Math.round(avgActualPerCall * 1_000_000) / 1_000_000,
3933
- avgCounterfactualPerCall: Math.round(avgCounterfactualPerCall * 1_000_000) / 1_000_000,
3934
- avgInputTokensPerCall: Math.round(totalInputTokens / pairCount),
3935
- avgOutputTokensPerCall: Math.round(totalOutputTokens / pairCount),
3936
- },
3937
- horizons,
3938
- };
3939
- if (fmt === 'json') {
3940
- output.writeln(JSON.stringify(payload, null, 2));
3941
- return { success: true, data: payload };
3942
- }
3943
- output.writeln();
3944
- output.writeln(output.bold('Cost projection (ADR-149 iter 41)'));
3945
- output.writeln(output.dim('─'.repeat(72)));
3946
- output.writeln(` Input: ${inPath}`);
3947
- output.writeln(` Measurement window: last ${windowSpec} (${pairCount} paired calls)`);
3948
- output.writeln('');
3949
- output.writeln(output.bold(' Measured rate:'));
3950
- output.writeln(` Calls/day: ${payload.measurement.callsPerDay}`);
3951
- output.writeln(` Avg actual cost/call: $${payload.measurement.avgActualPerCall.toFixed(6)}`);
3952
- output.writeln(` Avg counterfactual/call: $${payload.measurement.avgCounterfactualPerCall.toFixed(6)} (heuristic: cheap→haiku, mid→sonnet, strong→opus)`);
3953
- output.writeln(` Avg tokens/call: ${payload.measurement.avgInputTokensPerCall} in / ${payload.measurement.avgOutputTokensPerCall} out`);
3954
- output.writeln('');
3955
- output.writeln(output.bold(' Projections (linear extrapolation from measured rate):'));
3956
- output.writeln(' horizon projected calls actual $ counterfactual $ savings $ %');
3957
- for (const h of horizons) {
3958
- if ('error' in h) {
3959
- output.writeln(` ${h.spec.padEnd(7)} invalid duration`);
3960
- continue;
3961
- }
3962
- const savingsStr = h.projectedSavingsUsd >= 0
3963
- ? output.success(`$${h.projectedSavingsUsd.toFixed(2)}`)
3964
- : output.warning(`-$${Math.abs(h.projectedSavingsUsd).toFixed(2)}`);
3965
- output.writeln(` ${h.spec.padEnd(7)} ${String(h.projectedCalls).padStart(15)} $${h.projectedActualUsd.toFixed(2).padStart(12)} $${h.projectedCounterfactualUsd.toFixed(2).padStart(14)} ${savingsStr.padEnd(14)} ${h.projectedSavingsPct.toFixed(2).padStart(6)}%`);
3966
- }
3967
- output.writeln('');
3968
- output.writeln(output.dim(' Assumes the next horizon\'s workload mix and rate matches the measurement window.'));
3969
- output.writeln(output.dim(' Use iter 34 (--window) to check if recent windows are drifting before trusting these.'));
3970
- output.writeln('');
3971
- return { success: true, data: payload };
3972
- },
3973
- };
3974
- const routerCommand = {
3975
- name: 'router',
3976
- description: 'Cost-optimal neural router lifecycle (ADR-148/149): status, models, prices, config, train, train-from-trajectories, decide, compare-modes, decisions, cost-savings, cost-projection, trajectory-health, ab-stats, bandit-state, stats-summary, reload',
3977
- subcommands: [routerStatusCommand, routerModelsCommand, routerPricesCommand, routerConfigCommand, routerTrainCommand, routerTrainFromTrajectoriesCommand, routerDecideCommand, routerCompareModesCommand, routerDecisionsCommand, routerCostSavingsCommand, routerCostProjectionCommand, routerTrajectoryHealthCommand, routerAbStatsCommand, routerBanditStateCommand, routerStatsSummaryCommand, routerReloadCommand],
3978
- examples: [
3979
- { command: 'claude-flow neural router status', description: 'Show router state, gate, counters' },
3980
- { command: 'claude-flow neural router models', description: 'List candidate registry with measured stats (ADR-149)' },
3981
- { command: 'claude-flow neural router prices', description: 'Show the canonical $/Mtok price table (iter 43)' },
3982
- { command: 'claude-flow neural router config', description: 'Inventory all CLAUDE_FLOW_ROUTER_* env vars (iter 54)' },
3983
- { command: 'claude-flow neural router train -o ./router.krr.json', description: 'Train a KRR artifact' },
3984
- { command: 'claude-flow neural router train-from-trajectories -w production-rows.json', description: 'Pair production JSONL into a training corpus (iter 18)' },
3985
- { command: 'claude-flow neural router decide "fix typo in cache.ts"', description: 'Inspect decision for a hypothetical task (iter 30)' },
3986
- { command: 'claude-flow neural router compare-modes "task" --ceiling 5', description: 'Compare cost-optimal vs cost-ceiling side-by-side (iter 55)' },
3987
- { command: 'claude-flow neural router decisions --since 24h', description: 'Query recorded decisions (iter 28)' },
3988
- { command: 'claude-flow neural router cost-savings --since 7d', description: 'Actual vs heuristic-counterfactual spend (iter 32)' },
3989
- { command: 'claude-flow neural router trajectory-health', description: 'JSONL log health: size, rotations, parse + pair-join rate (iter 36)' },
3990
- { command: 'claude-flow neural router ab-stats', description: 'A/B disagreement matrix from sampled ab_pair (iter 37/38)' },
3991
- { command: 'claude-flow neural router bandit-state', description: 'Persisted Beta priors per (bucket × model) (iter 48)' },
3992
- { command: 'claude-flow neural router stats-summary', description: 'One-screen SRE dashboard (iter 49)' },
3993
- { command: 'claude-flow neural router cost-projection', description: 'Project monthly/quarterly spend from measured rate (iter 41)' },
3994
- { command: 'claude-flow neural router reload', description: 'Clear in-process backend cache' },
3995
- ],
3996
- action: async () => {
3997
- output.writeln('Use a subcommand: status | models | prices | config | train | train-from-trajectories | decide | compare-modes | decisions | cost-savings | cost-projection | trajectory-health | ab-stats | bandit-state | stats-summary | reload');
3998
- return { success: true };
3999
- },
4000
- };
4001
- // ============================================================================
4002
- // ADR-150 weight-eft slice — `neural distill export | plan | eval | train`
4003
- //
4004
- // Turns ruflo's captured run transcripts into AUDITED TRAINING DATA + a
4005
- // COST-PARETO measurement + a GPU TRAINING PLAN via the optional
4006
- // `@metaharness/weight-eft` dependency. HARD honesty rule: this ships training
4007
- // DATA + a cost audit + a GPU plan — it does NOT train a model and does NOT
4008
- // "reduce escalation". weight-eft's own `train` never spawns; `resolved` in the
4009
- // captured archive is a PROXY (no SWE-bench gold oracle). Every path degrades
4010
- // gracefully ({degraded:true}) when the optional dep is absent (ADR-150).
4011
- // ============================================================================
4012
- const distillExportCommand = {
4013
- name: 'export',
4014
- description: 'Export captured run transcripts → SFT (OpenAI chat) + DPO (TRL preference) JSONL + a guard report (contamination / reward-hack / long-context). $0, offline. Does NOT train.',
4015
- options: [
4016
- { name: 'archive', short: 'a', type: 'string', description: 'Run-transcript JSONL to read (default: $CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH or .swarm/run-transcripts.jsonl)' },
4017
- { name: 'out-dir', short: 'o', type: 'string', description: 'Output dir for sft.jsonl / dpo.jsonl / export-report.json', default: '.claude-flow/neural/weft-export' },
4018
- { name: 'eval-holdout', type: 'string', description: 'Comma-separated instance_ids reserved for eval (contamination guard). Excluded + asserted-disjoint.' },
4019
- { name: 'max-tokens', type: 'number', description: 'Per-trajectory token budget (default weight-eft 28000)' },
4020
- { name: 'truncate', type: 'boolean', description: 'Truncate over-length trajectories instead of dropping', default: 'false' },
4021
- { name: 'keep-reward-hacked', type: 'boolean', description: 'Disable the reward-hacking filter (debug only; NOT recommended)', default: 'false' },
4022
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
4023
- ],
4024
- examples: [
4025
- { command: 'claude-flow neural distill export', description: 'Export from the default captured .swarm/run-transcripts.jsonl' },
4026
- { command: 'claude-flow neural distill export -a runs.jsonl -o ./out --eval-holdout astropy__astropy-1', description: 'Export a specific archive, holding out one instance' },
4027
- ],
4028
- action: async (ctx) => {
4029
- const fs = await import('node:fs');
4030
- const path = await import('node:path');
4031
- const { readRunTranscripts } = await import('../ruvector/run-transcript-recorder.js');
4032
- const { buildArchiveFromRecords, runExport } = await import('../services/weight-eft.js');
4033
- const fmt = ctx.flags.format || 'table';
4034
- const archivePath = ctx.flags.archive
4035
- ?? process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH
4036
- ?? path.resolve(process.cwd(), '.swarm', 'run-transcripts.jsonl');
4037
- const { records, malformed } = readRunTranscripts(archivePath);
4038
- if (records.length === 0) {
4039
- const msg = `No run transcripts at ${archivePath}. Enable capture with CLAUDE_FLOW_RUN_TRANSCRIPTS=1, or pass --archive <file>.`;
4040
- if (fmt === 'json')
4041
- output.writeln(JSON.stringify({ ok: false, archivePath, records: 0, malformed, error: msg }, null, 2));
4042
- else
4043
- output.printError(msg);
4044
- return { success: false, exitCode: 1, data: { archivePath, records: 0 } };
4045
- }
4046
- const { trajectories, stats, proxyNote } = buildArchiveFromRecords(records);
4047
- const holdout = (ctx.flags['eval-holdout'] ?? ctx.flags.evalHoldout)?.split(',').map((s) => s.trim()).filter(Boolean) ?? [];
4048
- const maxTokens = (ctx.flags['max-tokens'] ?? ctx.flags.maxTokens) != null ? parseInt(String((ctx.flags['max-tokens'] ?? ctx.flags.maxTokens)), 10) : undefined;
4049
- const res = await runExport({
4050
- archive: trajectories,
4051
- evalHoldout: holdout,
4052
- maxTokens,
4053
- truncateOverLength: ctx.flags.truncate === true,
4054
- dropRewardHacked: (ctx.flags['keep-reward-hacked'] ?? ctx.flags.keepRewardHacked) === true ? false : undefined,
4055
- });
4056
- if (res.degraded) {
4057
- // ADR-150 graceful degradation: dep absent → not a runtime failure.
4058
- const payload = { degraded: true, reason: res.reason, archiveTrajectories: trajectories.length };
4059
- if (fmt === 'json')
4060
- output.writeln(JSON.stringify(payload, null, 2));
4061
- else {
4062
- output.writeln(output.warning(`weight-eft unavailable (${res.reason}).`));
4063
- output.writeln(output.dim('Install the optional dep: npm i @metaharness/weight-eft. Archive was built (' + trajectories.length + ' trajectories) but not exported.'));
4064
- }
4065
- return { success: true, exitCode: 0, data: payload };
4066
- }
4067
- const outDir = path.resolve(process.cwd(), (ctx.flags['out-dir'] ?? ctx.flags.outDir) || '.claude-flow/neural/weft-export');
4068
- fs.mkdirSync(outDir, { recursive: true });
4069
- const sftPath = path.join(outDir, 'sft.jsonl');
4070
- const dpoPath = path.join(outDir, 'dpo.jsonl');
4071
- const reportPath = path.join(outDir, 'export-report.json');
4072
- fs.writeFileSync(sftPath, res.sftJsonl);
4073
- fs.writeFileSync(dpoPath, res.dpoJsonl);
4074
- fs.writeFileSync(reportPath, JSON.stringify({ report: res.report, archiveStats: stats, proxyNote }, null, 2));
4075
- const payload = {
4076
- ok: true, archivePath, outDir, sftPath, dpoPath, reportPath,
4077
- sftRows: res.sftRows, dpoRows: res.dpoRows, malformed,
4078
- archiveStats: stats, report: res.report, proxyNote,
4079
- };
4080
- if (fmt === 'json') {
4081
- output.writeln(JSON.stringify(payload, null, 2));
4082
- return { success: true, data: payload };
4083
- }
4084
- output.writeln();
4085
- output.writeln(output.bold('weight-eft export — audited training data ($0, no model trained)'));
4086
- output.writeln(` archive: ${archivePath} (${records.length} records, ${malformed} malformed skipped)`);
4087
- output.writeln(` trajectories: ${stats.total} (cheap ${stats.byTier.cheap} / frontier ${stats.byTier.frontier}), resolved ${stats.resolved}`);
4088
- output.writeln(` SFT rows: ${res.sftRows} → ${sftPath}`);
4089
- output.writeln(` DPO rows: ${res.dpoRows} → ${dpoPath}`);
4090
- output.writeln(` guards: holdout=${res.report.excludedByHoldout} reward-hacked=${res.report.droppedRewardHacked} over-length=${res.report.droppedOverLength} truncated=${res.report.truncatedOverLength}`);
4091
- output.writeln(` report: ${reportPath}`);
4092
- output.writeln();
4093
- output.writeln(output.warning('resolved provenance: ' + JSON.stringify(stats.byResolvedSource)));
4094
- output.writeln(output.dim(proxyNote));
4095
- return { success: true, data: payload };
4096
- },
4097
- };
4098
- const distillPlanCommand = {
4099
- name: 'plan',
4100
- description: 'Print the two-stage (SFT → on-policy DPO) GPU training plan + the exact `ruvllm microlora` commands a GPU host would run. $0 dry-run — NEVER spawns a tune.',
4101
- options: [
4102
- { name: 'sft', type: 'string', description: 'Path to sft.jsonl (default: .claude-flow/neural/weft-export/sft.jsonl)' },
4103
- { name: 'dpo', type: 'string', description: 'Path to dpo.jsonl (default: .claude-flow/neural/weft-export/dpo.jsonl)' },
4104
- { name: 'base', short: 'b', type: 'string', description: 'Base model id to tune (7-14B band). Default Qwen2.5-Coder-7B-Instruct' },
4105
- { name: 'params-b', type: 'number', description: 'Base model param count in billions (gate [1,14]). Default 7' },
4106
- { name: 'adapter-prefix', type: 'string', description: 'Adapter output prefix', default: 'ruflo-weft' },
4107
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
4108
- ],
4109
- examples: [
4110
- { command: 'claude-flow neural distill plan', description: 'Print the GPU plan for the last export ($0 dry-run)' },
4111
- { command: 'claude-flow neural distill plan --base Qwen/Qwen2.5-Coder-7B-Instruct --params-b 7', description: 'Plan for a specific base model' },
4112
- ],
4113
- action: async (ctx) => {
4114
- const path = await import('node:path');
4115
- const { runPlan, DEFAULT_BASE_MODEL } = await import('../services/weight-eft.js');
4116
- const fmt = ctx.flags.format || 'table';
4117
- const sftPath = ctx.flags.sft || path.resolve(process.cwd(), '.claude-flow/neural/weft-export/sft.jsonl');
4118
- const dpoPath = ctx.flags.dpo || path.resolve(process.cwd(), '.claude-flow/neural/weft-export/dpo.jsonl');
4119
- const base = ctx.flags.base
4120
- ? { id: String(ctx.flags.base), paramsB: (ctx.flags['params-b'] ?? ctx.flags.paramsB) != null ? parseInt(String((ctx.flags['params-b'] ?? ctx.flags.paramsB)), 10) : 7 }
4121
- : DEFAULT_BASE_MODEL;
4122
- const res = await runPlan({ base, sftPath, dpoPath, adapterPrefix: String((ctx.flags['adapter-prefix'] ?? ctx.flags.adapterPrefix) || 'ruflo-weft') });
4123
- if (res.degraded) {
4124
- const payload = { degraded: true, reason: res.reason };
4125
- if (fmt === 'json')
4126
- output.writeln(JSON.stringify(payload, null, 2));
4127
- else {
4128
- output.writeln(output.warning(`weight-eft unavailable (${res.reason}).`));
4129
- output.writeln(output.dim('Install: npm i @metaharness/weight-eft'));
4130
- }
4131
- return { success: true, exitCode: 0, data: payload };
4132
- }
4133
- const payload = { ok: true, base: res.base, sft: res.sft, dpo: res.dpo, dryRun: true };
4134
- if (fmt === 'json') {
4135
- output.writeln(JSON.stringify(payload, null, 2));
4136
- return { success: true, data: payload };
4137
- }
4138
- output.writeln();
4139
- output.writeln(output.bold(`weight-eft GPU training plan ($0 dry-run — no tune runs from ruflo)`));
4140
- output.writeln(` base model: ${res.base.id} (${res.base.paramsB}B)`);
4141
- output.writeln(output.dim(' SFT stage:'));
4142
- output.writeln(` ${res.sft.summary}`);
4143
- output.writeln(` $ ${res.sft.command}`);
4144
- output.writeln(output.dim(' DPO stage (on-policy, init from SFT adapter):'));
4145
- output.writeln(` ${res.dpo.summary}`);
4146
- output.writeln(` $ ${res.dpo.command}`);
4147
- output.writeln();
4148
- output.writeln(output.dim('These commands run on a GPU host; ruflo does not execute them. See `neural distill train --remote` for a spend-gated remote path.'));
4149
- return { success: true, data: payload };
4150
- },
4151
- };
4152
- const distillEvalCommand = {
4153
- name: 'eval',
4154
- description: 'Fold two CascadeOutcome[] JSON files (base vs adapter) into the cost-Pareto delta — escalation-rate reduction + $/resolved. $0. Measures cost, does NOT claim a tune ran.',
4155
- options: [
4156
- { name: 'base-outcomes', type: 'string', description: 'JSON file: CascadeOutcome[] for the BASE cascade run', required: true },
4157
- { name: 'adapter-outcomes', type: 'string', description: 'JSON file: CascadeOutcome[] for the ADAPTER cascade run', required: true },
4158
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
4159
- ],
4160
- examples: [
4161
- { command: 'claude-flow neural distill eval --base-outcomes base.json --adapter-outcomes adapter.json', description: 'Cost-Pareto delta between base and adapter cascade runs' },
4162
- ],
4163
- action: async (ctx) => {
4164
- const fs = await import('node:fs');
4165
- const { runEval } = await import('../services/weight-eft.js');
4166
- const fmt = ctx.flags.format || 'table';
4167
- const basePath = (ctx.flags['base-outcomes'] ?? ctx.flags.baseOutcomes);
4168
- const adapterPath = (ctx.flags['adapter-outcomes'] ?? ctx.flags.adapterOutcomes);
4169
- if (!basePath || !adapterPath) {
4170
- output.printError('Both --base-outcomes and --adapter-outcomes are required.');
4171
- return { success: false, exitCode: 2 };
4172
- }
4173
- let baseOutcomes;
4174
- let adapterOutcomes;
4175
- try {
4176
- baseOutcomes = JSON.parse(fs.readFileSync(basePath, 'utf8'));
4177
- adapterOutcomes = JSON.parse(fs.readFileSync(adapterPath, 'utf8'));
4178
- }
4179
- catch (e) {
4180
- output.printError(`Failed to read outcome files: ${e.message}`);
4181
- return { success: false, exitCode: 1 };
4182
- }
4183
- if (!Array.isArray(baseOutcomes) || !Array.isArray(adapterOutcomes)) {
4184
- output.printError('Both files must contain a JSON array of CascadeOutcome objects.');
4185
- return { success: false, exitCode: 1 };
4186
- }
4187
- const res = await runEval({ baseOutcomes: baseOutcomes, adapterOutcomes: adapterOutcomes });
4188
- if (res.degraded) {
4189
- const payload = { degraded: true, reason: res.reason };
4190
- if (fmt === 'json')
4191
- output.writeln(JSON.stringify(payload, null, 2));
4192
- else {
4193
- output.writeln(output.warning(`weight-eft unavailable (${res.reason}).`));
4194
- output.writeln(output.dim('Install: npm i @metaharness/weight-eft'));
4195
- }
4196
- return { success: true, exitCode: 0, data: payload };
4197
- }
4198
- if (fmt === 'json') {
4199
- output.writeln(JSON.stringify({ ok: true, delta: res.delta }, null, 2));
4200
- return { success: true, data: res.delta };
4201
- }
4202
- output.writeln();
4203
- output.writeln(output.bold('weight-eft cost-Pareto delta (measurement only)'));
4204
- output.writeln(` cheap-resolve lift: ${res.delta.cheapResolveLift.toFixed(4)}`);
4205
- output.writeln(` escalation-rate reduction: ${res.delta.escalationRateReduction.toFixed(4)}`);
4206
- output.writeln(` $/resolved reduction: ${res.delta.costPerResolvedReduction.toFixed(6)}`);
4207
- output.writeln(` resolve-rate delta: ${res.delta.resolveRateDelta.toFixed(4)} (expected ≈ 0 — ceiling unmoved)`);
4208
- output.writeln(` verdict: ${res.delta.verdict}`);
4209
- return { success: true, data: res.delta };
4210
- },
4211
- };
4212
- const distillTrainCommand = {
4213
- name: 'train',
4214
- description: 'Remote-GPU LoRA tune over SSH — DRY-RUN by default (prints ssh/rsync/ruvllm commands + read-only preflight). Real compute ONLY with --execute --yes (spends GPU time on YOUR host). Not a $0/local tune.',
4215
- options: [
4216
- { name: 'remote', short: 'r', type: 'string', description: 'SSH host or tailscale name (default: $RUFLO_DISTILL_REMOTE). Never hard-coded.' },
4217
- { name: 'base', short: 'b', type: 'string', description: 'Base model id to tune. Default Qwen2.5-Coder-7B-Instruct' },
4218
- { name: 'sft', type: 'string', description: 'Local sft.jsonl (default: .claude-flow/neural/weft-export/sft.jsonl)' },
4219
- { name: 'dpo', type: 'string', description: 'Local dpo.jsonl (default: .claude-flow/neural/weft-export/dpo.jsonl)' },
4220
- { name: 'adapter-dir', type: 'string', description: 'Local dir to fetch the trained adapter into', default: '.claude-flow/neural' },
4221
- { name: 'ssh-user', type: 'string', description: 'SSH user (default: current user)' },
4222
- { name: 'ssh-port', type: 'number', description: 'SSH port', default: '22' },
4223
- { name: 'remote-workdir', type: 'string', description: 'Remote working dir (default: ~/.ruflo-weft/<runId>)' },
4224
- { name: 'execute', type: 'boolean', description: 'Opt in to REAL GPU compute on the remote host (still needs --yes)', default: 'false' },
4225
- { name: 'yes', type: 'boolean', description: 'Second confirmation gate; required with --execute to actually spend', default: 'false' },
4226
- { name: 'preflight', type: 'boolean', description: 'Opt in to read-only reachability/GPU probes against the host (bare dry-run is fully offline and contacts nothing)', default: 'false' },
4227
- { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
4228
- ],
4229
- examples: [
4230
- { command: 'claude-flow neural distill train --remote gpu-box', description: 'OFFLINE DRY-RUN: print the ssh/rsync/ruvllm commands only (no host contact)' },
4231
- { command: 'claude-flow neural distill train --remote gpu-box --preflight', description: 'DRY-RUN + read-only reachability/GPU probes against the host' },
4232
- { command: 'RUFLO_DISTILL_REMOTE=gpu-box claude-flow neural distill train --execute --yes', description: 'Run the real remote tune (spends GPU time on your host)' },
4233
- ],
4234
- action: async (ctx) => {
4235
- const path = await import('node:path');
4236
- const { runRemoteTrain } = await import('../services/weight-eft.js');
4237
- const fmt = ctx.flags.format || 'table';
4238
- const host = ctx.flags.remote || process.env.RUFLO_DISTILL_REMOTE;
4239
- if (!host) {
4240
- output.printError('No remote host. Pass --remote <host> or set RUFLO_DISTILL_REMOTE.');
4241
- return { success: false, exitCode: 2 };
4242
- }
4243
- const res = await runRemoteTrain({
4244
- host,
4245
- base: ctx.flags.base ? String(ctx.flags.base) : undefined,
4246
- sftPath: ctx.flags.sft || path.resolve(process.cwd(), '.claude-flow/neural/weft-export/sft.jsonl'),
4247
- dpoPath: ctx.flags.dpo || path.resolve(process.cwd(), '.claude-flow/neural/weft-export/dpo.jsonl'),
4248
- adapterDir: (ctx.flags['adapter-dir'] ?? ctx.flags.adapterDir) || '.claude-flow/neural',
4249
- sshUser: (ctx.flags['ssh-user'] ?? ctx.flags.sshUser) ? String((ctx.flags['ssh-user'] ?? ctx.flags.sshUser)) : undefined,
4250
- sshPort: (ctx.flags['ssh-port'] ?? ctx.flags.sshPort) != null ? parseInt(String((ctx.flags['ssh-port'] ?? ctx.flags.sshPort)), 10) : undefined,
4251
- remoteWorkdir: (ctx.flags['remote-workdir'] ?? ctx.flags.remoteWorkdir) ? String((ctx.flags['remote-workdir'] ?? ctx.flags.remoteWorkdir)) : undefined,
4252
- execute: ctx.flags.execute === true,
4253
- yes: ctx.flags.yes === true,
4254
- preflight: ctx.flags.preflight === true,
4255
- });
4256
- if ('degraded' in res && res.degraded) {
4257
- const payload = { degraded: true, reason: res.reason };
4258
- if (fmt === 'json')
4259
- output.writeln(JSON.stringify(payload, null, 2));
4260
- else
4261
- output.writeln(output.warning(`remote-train unavailable (${res.reason}).`));
4262
- return { success: true, exitCode: 0, data: payload };
4263
- }
4264
- if (fmt === 'json') {
4265
- output.writeln(JSON.stringify(res, null, 2));
4266
- return { success: res.mode !== 'preflight-failed', data: res };
4267
- }
4268
- output.writeln();
4269
- output.writeln(output.bold(`weight-eft remote-GPU tune [${res.mode}] on ${res.plan.host}`));
4270
- if (res.mode === 'dry-run')
4271
- output.writeln(output.dim('DRY-RUN — no data transferred, no training. Re-run with --execute --yes to spend GPU time on your host.'));
4272
- if (res.reason)
4273
- output.writeln(output.warning(res.reason));
4274
- output.writeln(` base: ${res.plan.base} remote workdir: ${res.plan.remoteWorkdir} adapter → ${res.plan.adapterDir}/${res.plan.dpoAdapter}`);
4275
- if (res.preflight) {
4276
- output.writeln(output.dim(' preflight (read-only probes):'));
4277
- for (const p of res.preflight)
4278
- output.writeln(` [${p.ok ? 'ok ' : 'FAIL'}] ${p.label}: ${p.detail}`);
4279
- }
4280
- output.writeln(output.dim(' commands that ' + (res.mode === 'executed' ? 'ran' : 'WOULD run') + ':'));
4281
- for (const c of res.plan.humanCommands)
4282
- output.writeln(` $ ${c}`);
4283
- if (res.steps) {
4284
- output.writeln(output.dim(' execution:'));
4285
- for (const s of res.steps)
4286
- output.writeln(` [${s.ok ? 'ok ' : 'FAIL'}] ${s.label}: ${s.detail}`);
4287
- }
4288
- output.writeln();
4289
- output.writeln(output.dim('Honesty: ruflo does not train locally or at $0. This is an explicit, user-triggered remote-GPU spend. resolved-gold in the SFT data is still a proxy.'));
4290
- return { success: res.mode !== 'preflight-failed', data: res };
4291
- },
4292
- };
4293
- const distillCommand = {
4294
- name: 'distill',
4295
- description: 'weight-eft training-data + cost-audit slice (ADR-150): export | plan | eval | train. Ships audited SFT/DPO data + a cost-Pareto measurement + a GPU plan. Does NOT train a model or reduce escalation.',
4296
- subcommands: [distillExportCommand, distillPlanCommand, distillEvalCommand, distillTrainCommand],
4297
- examples: [
4298
- { command: 'claude-flow neural distill export', description: 'Captured transcripts → audited SFT/DPO JSONL + guard report ($0)' },
4299
- { command: 'claude-flow neural distill plan', description: 'Print the GPU training plan + ruvllm commands ($0 dry-run)' },
4300
- { command: 'claude-flow neural distill eval --base-outcomes b.json --adapter-outcomes a.json', description: 'Cost-Pareto delta ($0)' },
4301
- { command: 'claude-flow neural distill train --remote gpu-box', description: 'Remote-GPU tune DRY-RUN (spend-gated behind --execute --yes)' },
4302
- ],
4303
- action: async () => {
4304
- output.writeln('Use a subcommand: export | plan | eval | train');
4305
- output.writeln(output.dim('Ships audited training DATA + a cost audit + a GPU plan. It does NOT train a model or reduce escalation (weight-eft train never spawns; resolved is a proxy).'));
4306
- return { success: true };
4307
- },
4308
- };
4309
- // Main neural command
4310
- export const neuralCommand = {
4311
- name: 'neural',
4312
- description: 'Neural pattern training, MoE, Flash Attention, pattern learning',
4313
- subcommands: [trainCommand, statusCommand, patternsCommand, predictCommand, optimizeCommand, benchmarkCommand, listCommand, exportCommand, importCommand, routerCommand, distillCommand],
4314
- examples: [
4315
- { command: 'claude-flow neural status', description: 'Check neural system status' },
4316
- { command: 'claude-flow neural train -p coordination', description: 'Train coordination patterns' },
4317
- { command: 'claude-flow neural patterns --action list', description: 'List learned patterns' },
4318
- ],
4319
- action: async () => {
4320
- output.writeln();
4321
- output.writeln(output.bold('RuFlo Neural System'));
4322
- output.writeln(output.dim('Advanced AI pattern learning and inference'));
4323
- output.writeln();
4324
- output.writeln('Use --help with subcommands for more info');
4325
- output.writeln();
4326
- output.writeln(output.dim('Created with ❤️ by ruv.io'));
4327
- return { success: true };
4328
- },
4329
- };
4330
- export default neuralCommand;
4331
- //# sourceMappingURL=neural.js.map