@claude-flow/cli 3.28.0 β†’ 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. package/.claude/agents/analysis/analyze-code-quality.md +178 -178
  2. package/.claude/agents/analysis/code-analyzer.md +209 -209
  3. package/.claude/agents/analysis/code-review/analyze-code-quality.md +178 -178
  4. package/.claude/agents/architecture/arch-system-design.md +156 -156
  5. package/.claude/agents/architecture/system-design/arch-system-design.md +154 -154
  6. package/.claude/agents/browser/browser-agent.yaml +182 -182
  7. package/.claude/agents/consensus/byzantine-coordinator.md +62 -62
  8. package/.claude/agents/consensus/crdt-synchronizer.md +996 -996
  9. package/.claude/agents/consensus/gossip-coordinator.md +62 -62
  10. package/.claude/agents/consensus/performance-benchmarker.md +850 -850
  11. package/.claude/agents/consensus/quorum-manager.md +822 -822
  12. package/.claude/agents/consensus/raft-manager.md +62 -62
  13. package/.claude/agents/consensus/security-manager.md +621 -621
  14. package/.claude/agents/core/planner.md +374 -374
  15. package/.claude/agents/custom/test-long-runner.md +44 -44
  16. package/.claude/agents/data/data-ml-model.md +444 -444
  17. package/.claude/agents/data/ml/data-ml-model.md +192 -192
  18. package/.claude/agents/development/backend/dev-backend-api.md +141 -141
  19. package/.claude/agents/development/dev-backend-api.md +344 -344
  20. package/.claude/agents/devops/ci-cd/ops-cicd-github.md +163 -163
  21. package/.claude/agents/devops/ops-cicd-github.md +164 -164
  22. package/.claude/agents/documentation/api-docs/docs-api-openapi.md +173 -173
  23. package/.claude/agents/documentation/docs-api-openapi.md +354 -354
  24. package/.claude/agents/flow-nexus/app-store.md +87 -87
  25. package/.claude/agents/flow-nexus/authentication.md +68 -68
  26. package/.claude/agents/flow-nexus/challenges.md +80 -80
  27. package/.claude/agents/flow-nexus/neural-network.md +87 -87
  28. package/.claude/agents/flow-nexus/payments.md +82 -82
  29. package/.claude/agents/flow-nexus/sandbox.md +75 -75
  30. package/.claude/agents/flow-nexus/swarm.md +75 -75
  31. package/.claude/agents/flow-nexus/user-tools.md +95 -95
  32. package/.claude/agents/flow-nexus/workflow.md +83 -83
  33. package/.claude/agents/github/code-review-swarm.md +377 -377
  34. package/.claude/agents/github/github-modes.md +172 -172
  35. package/.claude/agents/github/issue-tracker.md +575 -575
  36. package/.claude/agents/github/multi-repo-swarm.md +552 -552
  37. package/.claude/agents/github/pr-manager.md +437 -437
  38. package/.claude/agents/github/project-board-sync.md +508 -508
  39. package/.claude/agents/github/release-manager.md +604 -604
  40. package/.claude/agents/github/release-swarm.md +582 -582
  41. package/.claude/agents/github/repo-architect.md +397 -397
  42. package/.claude/agents/github/swarm-issue.md +572 -572
  43. package/.claude/agents/github/swarm-pr.md +427 -427
  44. package/.claude/agents/github/sync-coordinator.md +451 -451
  45. package/.claude/agents/github/workflow-automation.md +902 -902
  46. package/.claude/agents/goal/agent.md +815 -815
  47. package/.claude/agents/optimization/benchmark-suite.md +664 -664
  48. package/.claude/agents/optimization/load-balancer.md +430 -430
  49. package/.claude/agents/optimization/performance-monitor.md +671 -671
  50. package/.claude/agents/optimization/resource-allocator.md +673 -673
  51. package/.claude/agents/optimization/topology-optimizer.md +807 -807
  52. package/.claude/agents/payments/agentic-payments.md +126 -126
  53. package/.claude/agents/sona/sona-learning-optimizer.md +74 -74
  54. package/.claude/agents/sparc/architecture.md +698 -698
  55. package/.claude/agents/sparc/pseudocode.md +519 -519
  56. package/.claude/agents/sparc/refinement.md +801 -801
  57. package/.claude/agents/sparc/specification.md +477 -477
  58. package/.claude/agents/specialized/mobile/spec-mobile-react-native.md +224 -224
  59. package/.claude/agents/specialized/spec-mobile-react-native.md +226 -226
  60. package/.claude/agents/sublinear/consensus-coordinator.md +337 -337
  61. package/.claude/agents/sublinear/matrix-optimizer.md +184 -184
  62. package/.claude/agents/sublinear/pagerank-analyzer.md +298 -298
  63. package/.claude/agents/sublinear/performance-optimizer.md +367 -367
  64. package/.claude/agents/sublinear/trading-predictor.md +245 -245
  65. package/.claude/agents/swarm/adaptive-coordinator.md +1126 -1126
  66. package/.claude/agents/swarm/hierarchical-coordinator.md +709 -709
  67. package/.claude/agents/swarm/mesh-coordinator.md +962 -962
  68. package/.claude/agents/templates/automation-smart-agent.md +204 -204
  69. package/.claude/agents/templates/base-template-generator.md +289 -289
  70. package/.claude/agents/templates/coordinator-swarm-init.md +89 -89
  71. package/.claude/agents/templates/github-pr-manager.md +176 -176
  72. package/.claude/agents/templates/implementer-sparc-coder.md +258 -258
  73. package/.claude/agents/templates/memory-coordinator.md +186 -186
  74. package/.claude/agents/templates/orchestrator-task.md +138 -138
  75. package/.claude/agents/templates/performance-analyzer.md +198 -198
  76. package/.claude/agents/templates/sparc-coordinator.md +513 -513
  77. package/.claude/agents/testing/production-validator.md +394 -394
  78. package/.claude/agents/testing/tdd-london-swarm.md +243 -243
  79. package/.claude/agents/v3/aidefence-guardian.md +282 -282
  80. package/.claude/agents/v3/claims-authorizer.md +208 -208
  81. package/.claude/agents/v3/collective-intelligence-coordinator.md +993 -993
  82. package/.claude/agents/v3/ddd-domain-expert.md +220 -220
  83. package/.claude/agents/v3/injection-analyst.md +236 -236
  84. package/.claude/agents/v3/performance-engineer.md +1233 -1233
  85. package/.claude/agents/v3/pii-detector.md +151 -151
  86. package/.claude/agents/v3/reasoningbank-learner.md +213 -213
  87. package/.claude/agents/v3/security-architect-aidefence.md +410 -410
  88. package/.claude/agents/v3/security-architect.md +867 -867
  89. package/.claude/agents/v3/swarm-memory-manager.md +157 -157
  90. package/.claude/agents/v3/v3-integration-architect.md +205 -205
  91. package/.claude/commands/agents/README.md +50 -50
  92. package/.claude/commands/agents/agent-capabilities.md +140 -140
  93. package/.claude/commands/agents/agent-coordination.md +28 -28
  94. package/.claude/commands/agents/agent-spawning.md +28 -28
  95. package/.claude/commands/agents/agent-types.md +216 -216
  96. package/.claude/commands/agents/health.md +139 -139
  97. package/.claude/commands/agents/list.md +100 -100
  98. package/.claude/commands/agents/logs.md +130 -130
  99. package/.claude/commands/agents/metrics.md +122 -122
  100. package/.claude/commands/agents/pool.md +127 -127
  101. package/.claude/commands/agents/spawn.md +140 -140
  102. package/.claude/commands/agents/status.md +115 -115
  103. package/.claude/commands/agents/stop.md +102 -102
  104. package/.claude/commands/analysis/COMMAND_COMPLIANCE_REPORT.md +53 -53
  105. package/.claude/commands/analysis/README.md +9 -9
  106. package/.claude/commands/analysis/bottleneck-detect.md +162 -162
  107. package/.claude/commands/analysis/performance-bottlenecks.md +58 -58
  108. package/.claude/commands/analysis/performance-report.md +25 -25
  109. package/.claude/commands/analysis/token-efficiency.md +44 -44
  110. package/.claude/commands/analysis/token-usage.md +25 -25
  111. package/.claude/commands/automation/README.md +9 -9
  112. package/.claude/commands/automation/auto-agent.md +122 -122
  113. package/.claude/commands/automation/self-healing.md +105 -105
  114. package/.claude/commands/automation/session-memory.md +89 -89
  115. package/.claude/commands/automation/smart-agents.md +72 -72
  116. package/.claude/commands/automation/smart-spawn.md +25 -25
  117. package/.claude/commands/automation/workflow-select.md +25 -25
  118. package/.claude/commands/claude-flow-help.md +103 -103
  119. package/.claude/commands/claude-flow-memory.md +107 -107
  120. package/.claude/commands/claude-flow-swarm.md +205 -205
  121. package/.claude/commands/coordination/README.md +9 -9
  122. package/.claude/commands/coordination/agent-spawn.md +25 -25
  123. package/.claude/commands/coordination/init.md +44 -44
  124. package/.claude/commands/coordination/orchestrate.md +43 -43
  125. package/.claude/commands/coordination/spawn.md +45 -45
  126. package/.claude/commands/coordination/swarm-init.md +85 -85
  127. package/.claude/commands/coordination/task-orchestrate.md +25 -25
  128. package/.claude/commands/github/README.md +11 -11
  129. package/.claude/commands/github/code-review-swarm.md +513 -513
  130. package/.claude/commands/github/code-review.md +25 -25
  131. package/.claude/commands/github/github-modes.md +146 -146
  132. package/.claude/commands/github/github-swarm.md +121 -121
  133. package/.claude/commands/github/issue-tracker.md +291 -291
  134. package/.claude/commands/github/issue-triage.md +25 -25
  135. package/.claude/commands/github/multi-repo-swarm.md +518 -518
  136. package/.claude/commands/github/pr-enhance.md +26 -26
  137. package/.claude/commands/github/pr-manager.md +169 -169
  138. package/.claude/commands/github/project-board-sync.md +470 -470
  139. package/.claude/commands/github/release-manager.md +339 -339
  140. package/.claude/commands/github/release-swarm.md +543 -543
  141. package/.claude/commands/github/repo-analyze.md +25 -25
  142. package/.claude/commands/github/repo-architect.md +366 -366
  143. package/.claude/commands/github/swarm-issue.md +484 -484
  144. package/.claude/commands/github/swarm-pr.md +287 -287
  145. package/.claude/commands/github/sync-coordinator.md +302 -302
  146. package/.claude/commands/github/workflow-automation.md +441 -441
  147. package/.claude/commands/hive-mind/README.md +17 -17
  148. package/.claude/commands/hive-mind/hive-mind-consensus.md +8 -8
  149. package/.claude/commands/hive-mind/hive-mind-init.md +18 -18
  150. package/.claude/commands/hive-mind/hive-mind-memory.md +8 -8
  151. package/.claude/commands/hive-mind/hive-mind-metrics.md +8 -8
  152. package/.claude/commands/hive-mind/hive-mind-resume.md +8 -8
  153. package/.claude/commands/hive-mind/hive-mind-sessions.md +8 -8
  154. package/.claude/commands/hive-mind/hive-mind-spawn.md +21 -21
  155. package/.claude/commands/hive-mind/hive-mind-status.md +8 -8
  156. package/.claude/commands/hive-mind/hive-mind-stop.md +8 -8
  157. package/.claude/commands/hive-mind/hive-mind-wizard.md +8 -8
  158. package/.claude/commands/hive-mind/hive-mind.md +27 -27
  159. package/.claude/commands/hooks/README.md +11 -11
  160. package/.claude/commands/hooks/overview.md +57 -57
  161. package/.claude/commands/hooks/post-edit.md +117 -117
  162. package/.claude/commands/hooks/post-task.md +112 -112
  163. package/.claude/commands/hooks/pre-edit.md +113 -113
  164. package/.claude/commands/hooks/pre-task.md +111 -111
  165. package/.claude/commands/hooks/session-end.md +118 -118
  166. package/.claude/commands/hooks/setup.md +102 -102
  167. package/.claude/commands/memory/README.md +9 -9
  168. package/.claude/commands/memory/memory-persist.md +25 -25
  169. package/.claude/commands/memory/memory-search.md +25 -25
  170. package/.claude/commands/memory/memory-usage.md +25 -25
  171. package/.claude/commands/memory/neural.md +47 -47
  172. package/.claude/commands/monitoring/README.md +9 -9
  173. package/.claude/commands/monitoring/agent-metrics.md +25 -25
  174. package/.claude/commands/monitoring/agents.md +44 -44
  175. package/.claude/commands/monitoring/real-time-view.md +25 -25
  176. package/.claude/commands/monitoring/status.md +46 -46
  177. package/.claude/commands/monitoring/swarm-monitor.md +25 -25
  178. package/.claude/commands/optimization/README.md +9 -9
  179. package/.claude/commands/optimization/auto-topology.md +61 -61
  180. package/.claude/commands/optimization/cache-manage.md +25 -25
  181. package/.claude/commands/optimization/parallel-execute.md +25 -25
  182. package/.claude/commands/optimization/parallel-execution.md +49 -49
  183. package/.claude/commands/optimization/topology-optimize.md +25 -25
  184. package/.claude/commands/pair/README.md +260 -260
  185. package/.claude/commands/pair/commands.md +545 -545
  186. package/.claude/commands/pair/config.md +509 -509
  187. package/.claude/commands/pair/examples.md +511 -511
  188. package/.claude/commands/pair/modes.md +347 -347
  189. package/.claude/commands/pair/session.md +406 -406
  190. package/.claude/commands/pair/start.md +208 -208
  191. package/.claude/commands/sparc/analyzer.md +51 -51
  192. package/.claude/commands/sparc/architect.md +53 -53
  193. package/.claude/commands/sparc/ask.md +97 -97
  194. package/.claude/commands/sparc/batch-executor.md +54 -54
  195. package/.claude/commands/sparc/code.md +89 -89
  196. package/.claude/commands/sparc/coder.md +54 -54
  197. package/.claude/commands/sparc/debug.md +83 -83
  198. package/.claude/commands/sparc/debugger.md +54 -54
  199. package/.claude/commands/sparc/designer.md +53 -53
  200. package/.claude/commands/sparc/devops.md +109 -109
  201. package/.claude/commands/sparc/docs-writer.md +80 -80
  202. package/.claude/commands/sparc/documenter.md +54 -54
  203. package/.claude/commands/sparc/innovator.md +54 -54
  204. package/.claude/commands/sparc/integration.md +83 -83
  205. package/.claude/commands/sparc/mcp.md +117 -117
  206. package/.claude/commands/sparc/memory-manager.md +54 -54
  207. package/.claude/commands/sparc/optimizer.md +54 -54
  208. package/.claude/commands/sparc/orchestrator.md +131 -131
  209. package/.claude/commands/sparc/post-deployment-monitoring-mode.md +83 -83
  210. package/.claude/commands/sparc/refinement-optimization-mode.md +83 -83
  211. package/.claude/commands/sparc/researcher.md +54 -54
  212. package/.claude/commands/sparc/reviewer.md +54 -54
  213. package/.claude/commands/sparc/security-review.md +80 -80
  214. package/.claude/commands/sparc/sparc-modes.md +174 -174
  215. package/.claude/commands/sparc/sparc.md +111 -111
  216. package/.claude/commands/sparc/spec-pseudocode.md +80 -80
  217. package/.claude/commands/sparc/supabase-admin.md +348 -348
  218. package/.claude/commands/sparc/swarm-coordinator.md +54 -54
  219. package/.claude/commands/sparc/tdd.md +54 -54
  220. package/.claude/commands/sparc/tester.md +54 -54
  221. package/.claude/commands/sparc/tutorial.md +79 -79
  222. package/.claude/commands/sparc/workflow-manager.md +54 -54
  223. package/.claude/commands/sparc.md +166 -166
  224. package/.claude/commands/stream-chain/pipeline.md +120 -120
  225. package/.claude/commands/stream-chain/run.md +69 -69
  226. package/.claude/commands/swarm/README.md +15 -15
  227. package/.claude/commands/swarm/analysis.md +95 -95
  228. package/.claude/commands/swarm/development.md +96 -96
  229. package/.claude/commands/swarm/examples.md +168 -168
  230. package/.claude/commands/swarm/maintenance.md +102 -102
  231. package/.claude/commands/swarm/optimization.md +117 -117
  232. package/.claude/commands/swarm/research.md +136 -136
  233. package/.claude/commands/swarm/swarm-analysis.md +8 -8
  234. package/.claude/commands/swarm/swarm-background.md +8 -8
  235. package/.claude/commands/swarm/swarm-init.md +19 -19
  236. package/.claude/commands/swarm/swarm-modes.md +8 -8
  237. package/.claude/commands/swarm/swarm-monitor.md +8 -8
  238. package/.claude/commands/swarm/swarm-spawn.md +19 -19
  239. package/.claude/commands/swarm/swarm-status.md +8 -8
  240. package/.claude/commands/swarm/swarm-strategies.md +8 -8
  241. package/.claude/commands/swarm/swarm.md +87 -87
  242. package/.claude/commands/swarm/testing.md +131 -131
  243. package/.claude/commands/training/README.md +9 -9
  244. package/.claude/commands/training/model-update.md +25 -25
  245. package/.claude/commands/training/neural-patterns.md +107 -107
  246. package/.claude/commands/training/neural-train.md +75 -75
  247. package/.claude/commands/training/pattern-learn.md +25 -25
  248. package/.claude/commands/training/specialization.md +62 -62
  249. package/.claude/commands/truth/start.md +142 -142
  250. package/.claude/commands/verify/check.md +49 -49
  251. package/.claude/commands/verify/start.md +127 -127
  252. package/.claude/commands/workflows/README.md +9 -9
  253. package/.claude/commands/workflows/development.md +77 -77
  254. package/.claude/commands/workflows/research.md +62 -62
  255. package/.claude/commands/workflows/workflow-create.md +25 -25
  256. package/.claude/commands/workflows/workflow-execute.md +25 -25
  257. package/.claude/commands/workflows/workflow-export.md +25 -25
  258. package/.claude/eval/human-relevance-frozen-v1.json +17 -17
  259. package/.claude/evolve-proof/generation-0.json +211 -211
  260. package/.claude/evolve-proof/real-generation-0.json +406 -406
  261. package/.claude/evolve-proof/real-generation-1.json +406 -406
  262. package/.claude/helpers/.helpers-version +1 -1
  263. package/.claude/helpers/README.md +96 -96
  264. package/.claude/helpers/adr-compliance.sh +186 -186
  265. package/.claude/helpers/auto-commit.sh +178 -178
  266. package/.claude/helpers/auto-memory-hook.mjs +430 -430
  267. package/.claude/helpers/checkpoint-manager.sh +251 -251
  268. package/.claude/helpers/daemon-manager.sh +252 -252
  269. package/.claude/helpers/ddd-tracker.sh +144 -144
  270. package/.claude/helpers/github-safe.js +156 -156
  271. package/.claude/helpers/github-setup.sh +45 -45
  272. package/.claude/helpers/guidance-hook.sh +13 -13
  273. package/.claude/helpers/guidance-hooks.sh +102 -102
  274. package/.claude/helpers/health-monitor.sh +108 -108
  275. package/.claude/helpers/helpers.manifest.json +6 -6
  276. package/.claude/helpers/hook-handler.cjs +564 -460
  277. package/.claude/helpers/intelligence.cjs +1058 -1058
  278. package/.claude/helpers/learning-hooks.sh +329 -329
  279. package/.claude/helpers/learning-optimizer.sh +127 -127
  280. package/.claude/helpers/learning-service.mjs +1144 -1144
  281. package/.claude/helpers/memory.js +83 -83
  282. package/.claude/helpers/metrics-db.mjs +503 -503
  283. package/.claude/helpers/pattern-consolidator.sh +86 -86
  284. package/.claude/helpers/perf-worker.sh +160 -160
  285. package/.claude/helpers/post-commit +16 -16
  286. package/.claude/helpers/pre-commit +26 -26
  287. package/.claude/helpers/quick-start.sh +19 -19
  288. package/.claude/helpers/router.js +105 -105
  289. package/.claude/helpers/security-scanner.sh +127 -127
  290. package/.claude/helpers/session.js +157 -157
  291. package/.claude/helpers/setup-mcp.sh +18 -18
  292. package/.claude/helpers/standard-checkpoint-hooks.sh +189 -189
  293. package/.claude/helpers/statusline-hook.sh +21 -21
  294. package/.claude/helpers/statusline.cjs +925 -925
  295. package/.claude/helpers/statusline.js +352 -352
  296. package/.claude/helpers/swarm-comms.sh +353 -353
  297. package/.claude/helpers/swarm-hooks.sh +761 -761
  298. package/.claude/helpers/swarm-monitor.sh +210 -210
  299. package/.claude/helpers/sync-v3-metrics.sh +245 -245
  300. package/.claude/helpers/update-v3-progress.sh +165 -165
  301. package/.claude/helpers/v3-quick-status.sh +57 -57
  302. package/.claude/helpers/v3.sh +110 -110
  303. package/.claude/helpers/validate-v3-config.sh +215 -215
  304. package/.claude/helpers/worker-manager.sh +170 -170
  305. package/.claude/proven-config.json +1 -1
  306. package/.claude/proven-config.manifest.json +37 -37
  307. package/.claude/proven-config.signed.json +41 -41
  308. package/.claude/settings.json +182 -182
  309. package/.claude/skills/agentdb-advanced/SKILL.md +550 -550
  310. package/.claude/skills/agentdb-learning/SKILL.md +545 -545
  311. package/.claude/skills/agentdb-memory-patterns/SKILL.md +339 -339
  312. package/.claude/skills/agentdb-optimization/SKILL.md +509 -509
  313. package/.claude/skills/agentdb-vector-search/SKILL.md +339 -339
  314. package/.claude/skills/browser/SKILL.md +204 -204
  315. package/.claude/skills/dual-mode/README.md +71 -71
  316. package/.claude/skills/dual-mode/dual-collect.md +103 -103
  317. package/.claude/skills/dual-mode/dual-coordinate.md +85 -85
  318. package/.claude/skills/dual-mode/dual-spawn.md +81 -81
  319. package/.claude/skills/flow-nexus-neural/SKILL.md +727 -727
  320. package/.claude/skills/flow-nexus-platform/SKILL.md +1154 -1154
  321. package/.claude/skills/flow-nexus-swarm/SKILL.md +604 -604
  322. package/.claude/skills/github-code-review/SKILL.md +1125 -1125
  323. package/.claude/skills/github-multi-repo/SKILL.md +862 -862
  324. package/.claude/skills/github-project-management/SKILL.md +1262 -1262
  325. package/.claude/skills/github-release-management/SKILL.md +1064 -1064
  326. package/.claude/skills/github-workflow-automation/SKILL.md +1047 -1047
  327. package/.claude/skills/hooks-automation/SKILL.md +1201 -1201
  328. package/.claude/skills/pair-programming/SKILL.md +1202 -1202
  329. package/.claude/skills/reasoningbank-agentdb/SKILL.md +446 -446
  330. package/.claude/skills/reasoningbank-intelligence/SKILL.md +201 -201
  331. package/.claude/skills/skill-builder/SKILL.md +910 -910
  332. package/.claude/skills/sparc-methodology/SKILL.md +1106 -1106
  333. package/.claude/skills/stream-chain/SKILL.md +560 -560
  334. package/.claude/skills/swarm-advanced/SKILL.md +970 -970
  335. package/.claude/skills/swarm-orchestration/SKILL.md +179 -179
  336. package/.claude/skills/v3-cli-modernization/SKILL.md +871 -871
  337. package/.claude/skills/v3-core-implementation/SKILL.md +796 -796
  338. package/.claude/skills/v3-ddd-architecture/SKILL.md +441 -441
  339. package/.claude/skills/v3-integration-deep/SKILL.md +240 -240
  340. package/.claude/skills/v3-mcp-optimization/SKILL.md +776 -776
  341. package/.claude/skills/v3-memory-unification/SKILL.md +173 -173
  342. package/.claude/skills/v3-performance-optimization/SKILL.md +389 -389
  343. package/.claude/skills/v3-security-overhaul/SKILL.md +81 -81
  344. package/.claude/skills/v3-swarm-coordination/SKILL.md +339 -339
  345. package/.claude/skills/verification-quality/SKILL.md +691 -691
  346. package/README.md +419 -419
  347. package/bin/cli.js +314 -314
  348. package/bin/mcp-server.js +224 -224
  349. package/bin/preinstall.cjs +2 -2
  350. package/catalog-manifest.json +2 -2
  351. package/dist/src/benchmarks/gaia-critic.js +24 -24
  352. package/dist/src/business-pods/bbs-budget-tracker.js +53 -53
  353. package/dist/src/commands/announcements.d.ts +17 -0
  354. package/dist/src/commands/announcements.js +0 -0
  355. package/dist/src/commands/completions.js +409 -409
  356. package/dist/src/commands/daemon.js +44 -44
  357. package/dist/src/commands/embeddings.js +26 -26
  358. package/dist/src/commands/funnel.d.ts +10 -5
  359. package/dist/src/commands/funnel.js +206 -7
  360. package/dist/src/commands/hive-mind.js +97 -97
  361. package/dist/src/commands/hooks.js +9 -9
  362. package/dist/src/commands/index.js +4 -0
  363. package/dist/src/commands/init.js +94 -38
  364. package/dist/src/commands/memory.js +85 -1
  365. package/dist/src/commands/ruvector/backup.js +23 -23
  366. package/dist/src/commands/ruvector/benchmark.js +31 -31
  367. package/dist/src/commands/ruvector/import.js +14 -14
  368. package/dist/src/commands/ruvector/init.js +115 -115
  369. package/dist/src/commands/ruvector/migrate.js +99 -99
  370. package/dist/src/commands/ruvector/optimize.js +51 -51
  371. package/dist/src/commands/ruvector/setup.js +624 -624
  372. package/dist/src/commands/ruvector/status.js +38 -38
  373. package/dist/src/commands/spinner.d.ts +16 -0
  374. package/dist/src/commands/spinner.js +329 -0
  375. package/dist/src/config/proven-config.js +2 -2
  376. package/dist/src/funnel/consent.js +3 -0
  377. package/dist/src/funnel/disclosure.d.ts +1 -0
  378. package/dist/src/funnel/disclosure.js +12 -0
  379. package/dist/src/funnel/index.d.ts +2 -1
  380. package/dist/src/funnel/index.js +2 -1
  381. package/dist/src/funnel/payout.d.ts +40 -0
  382. package/dist/src/funnel/payout.js +60 -0
  383. package/dist/src/funnel/types.d.ts +13 -1
  384. package/dist/src/init/claudemd-generator.js +231 -231
  385. package/dist/src/init/executor.js +453 -453
  386. package/dist/src/init/helper-refresh.js +17 -0
  387. package/dist/src/init/helper-signing.d.ts +8 -1
  388. package/dist/src/init/helper-signing.js +9 -2
  389. package/dist/src/init/helpers-generator.js +751 -751
  390. package/dist/src/init/statusline-generator.js +955 -949
  391. package/dist/src/mcp-tools/agentdb-tools.js +15 -15
  392. package/dist/src/mcp-tools/browser-intent-tools.js +19 -19
  393. package/dist/src/memory/graph-edge-writer.js +22 -22
  394. package/dist/src/memory/memory-bridge.d.ts +10 -0
  395. package/dist/src/memory/memory-bridge.js +139 -88
  396. package/dist/src/memory/memory-initializer.d.ts +18 -0
  397. package/dist/src/memory/memory-initializer.js +539 -407
  398. package/dist/src/memory/rabitq-index.js +5 -5
  399. package/dist/src/runtime/headless.js +28 -28
  400. package/dist/src/ruvector/flash-attention.d.ts +195 -0
  401. package/dist/src/ruvector/flash-attention.js +643 -0
  402. package/dist/src/ruvector/moe-router.d.ts +206 -0
  403. package/dist/src/ruvector/moe-router.js +626 -0
  404. package/dist/src/services/distill-tuning.js +7 -7
  405. package/dist/src/services/event-stream.d.ts +25 -0
  406. package/dist/src/services/event-stream.js +27 -0
  407. package/dist/src/services/headless-worker-executor.js +84 -84
  408. package/dist/src/services/loop-worker-runner.d.ts +16 -0
  409. package/dist/src/services/loop-worker-runner.js +34 -0
  410. package/dist/src/services/memory-distillation.js +4 -4
  411. package/dist/src/services/runtime-capabilities.d.ts +22 -0
  412. package/dist/src/services/runtime-capabilities.js +45 -0
  413. package/dist/src/transfer/deploy-seraphine.js +23 -23
  414. package/package.json +134 -133
  415. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +32 -32
  416. package/plugins/ruflo-metaharness/README.md +72 -72
  417. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +58 -58
  418. package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +48 -48
  419. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +210 -210
  420. package/plugins/ruflo-metaharness/scripts/_harness.mjs +330 -330
  421. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +231 -231
  422. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +143 -143
  423. package/plugins/ruflo-metaharness/scripts/_similarity.mjs +161 -161
  424. package/plugins/ruflo-metaharness/scripts/_spike-similarity.mjs +223 -223
  425. package/plugins/ruflo-metaharness/scripts/audit-list.mjs +158 -158
  426. package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +272 -272
  427. package/plugins/ruflo-metaharness/scripts/bench-parse-mcp-scan.mjs +146 -146
  428. package/plugins/ruflo-metaharness/scripts/bench-recordpair-overhead.mjs +186 -186
  429. package/plugins/ruflo-metaharness/scripts/bench-similarity.mjs +177 -177
  430. package/plugins/ruflo-metaharness/scripts/bench.mjs +95 -95
  431. package/plugins/ruflo-metaharness/scripts/drift-from-history.mjs +363 -363
  432. package/plugins/ruflo-metaharness/scripts/evolve.mjs +404 -404
  433. package/plugins/ruflo-metaharness/scripts/genome.mjs +80 -80
  434. package/plugins/ruflo-metaharness/scripts/gepa.mjs +153 -153
  435. package/plugins/ruflo-metaharness/scripts/learn.mjs +127 -127
  436. package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +111 -111
  437. package/plugins/ruflo-metaharness/scripts/mint.mjs +126 -126
  438. package/plugins/ruflo-metaharness/scripts/oia-audit.mjs +228 -228
  439. package/plugins/ruflo-metaharness/scripts/redblue.mjs +286 -286
  440. package/plugins/ruflo-metaharness/scripts/router-parallel-analyze.mjs +250 -250
  441. package/plugins/ruflo-metaharness/scripts/score.mjs +92 -92
  442. package/plugins/ruflo-metaharness/scripts/security-bench.mjs +174 -174
  443. package/plugins/ruflo-metaharness/scripts/similarity.mjs +158 -158
  444. package/plugins/ruflo-metaharness/scripts/smoke.sh +2353 -2353
  445. package/plugins/ruflo-metaharness/scripts/test-graceful-degradation.mjs +165 -165
  446. package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +472 -472
  447. package/plugins/ruflo-metaharness/scripts/test-parallel-pipeline.mjs +204 -204
  448. package/plugins/ruflo-metaharness/scripts/test-pipeline-roundtrip.mjs +586 -586
  449. package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +334 -334
  450. package/plugins/ruflo-metaharness/scripts/test-with-openrouter.mjs +229 -229
  451. package/plugins/ruflo-metaharness/scripts/threat-model.mjs +59 -59
  452. package/plugins/ruflo-metaharness/skills/harness-bench/SKILL.md +64 -64
  453. package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +65 -65
  454. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +131 -131
  455. package/plugins/ruflo-metaharness/skills/harness-genome/SKILL.md +54 -54
  456. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +65 -65
  457. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +65 -65
  458. package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +49 -49
  459. package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +72 -72
  460. package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +79 -79
  461. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +66 -66
  462. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +101 -101
  463. package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +67 -67
  464. package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +41 -41
  465. package/scripts/postinstall.cjs +153 -153
@@ -1,925 +1,925 @@
1
- #!/usr/bin/env node
2
- /**
3
- * RuFlo V3 Statusline β€” delegation build (#2195)
4
- *
5
- * Fix for ruvnet/ruflo#2195: the previous version re-implemented all data
6
- * readers locally using fragile file probes that missed AgentDB patterns,
7
- * the v3/docs/adr/ ADR directory, and the real vector count.
8
- *
9
- * This version delegates to 'npx @claude-flow/cli hooks statusline --json'
10
- * as the single source of truth. That command queries AgentDB directly,
11
- * counts ADRs in both directories, and reports the real intelligence pct.
12
- *
13
- * ADR counting falls back to local file reads so the display still works
14
- * without network access (counts both v3/docs/adr/ and v3/implementation/adrs/).
15
- *
16
- * Cache: JSON result is cached in /tmp for 10s so rapid prompt triggers
17
- * (every keystroke in some shells) don't hammer the CLI on every call.
18
- *
19
- * Usage: node statusline.cjs [--json] [--compact] [--dashboard]
20
- */
21
-
22
- /* eslint-disable @typescript-eslint/no-var-requires */
23
- const fs = require('fs');
24
- const path = require('path');
25
- const { execSync } = require('child_process');
26
- const os = require('os');
27
-
28
- // Configuration
29
- const CONFIG = {
30
- maxAgents: 15,
31
- // Session-cost display. Claude Code's cost.total_cost_usd is a client-side
32
- // estimate that "may differ from your actual bill" and reads as misleading on
33
- // subscription plans, where token usage is not billed per dollar. These let
34
- // each user pick what the segment means to them without changing the default.
35
- // RUFLO_STATUSLINE_COST_SYMBOL override the leading '$' (e.g. ⚑, €, 🌱);
36
- // set to an empty string for the number alone.
37
- // RUFLO_STATUSLINE_HIDE_COST 1/true/yes/on removes the segment entirely.
38
- costSymbol: process.env.RUFLO_STATUSLINE_COST_SYMBOL ?? '$',
39
- hideCost: /^(1|true|yes|on)$/i.test(process.env.RUFLO_STATUSLINE_HIDE_COST || ''),
40
- };
41
-
42
- const CWD = process.cwd();
43
-
44
- // ─── Delegation cache ───────────────────────────────────────────
45
- // Cache the CLI JSON result for 60s so rapid prompt re-renders
46
- // (Claude Code refreshes the statusline several times a second while
47
- // streaming) don't re-invoke the CLI each time. #2337: bumped 10s→60s
48
- // because 10s was far too short for how often Claude Code re-renders.
49
- const CACHE_FILE = path.join(os.tmpdir(), 'ruflo-statusline-cache-' + require('crypto').createHash('md5').update(CWD).digest('hex').slice(0, 8) + '.json');
50
- const CACHE_TTL_MS = 60000;
51
-
52
- // The promo/insight row is designed to rotate on a 20s cadence (funnel/
53
- // rotation.ts's ROTATION_SLOT_MS / funnel/promo.ts's insight-slot check β€”
54
- // duplicated here as a bare number since this generated script has no
55
- // runtime import of the funnel module; keep in sync if that constant ever
56
- // changes). The rotation slot is only ever (re)computed SERVER-SIDE inside
57
- // the CLI subprocess this file shells out to β€” so a general 60s data cache
58
- // (correct and necessary for #2337) silently made that 20s design
59
- // unreachable: cache.fresh stayed true across 2-3 whole rotation slots,
60
- // so the row visibly "didn't rotate" (user report). Fix: track promo
61
- // freshness on its OWN, tighter clock β€” when it lags behind the current
62
- // slot, fall through to a real CLI call even though the REST of the
63
- // cached data (security/swarm/system) is still within CACHE_TTL_MS. This
64
- // does not touch or regress #2337's fix; it only adds a narrower check.
65
- const PROMO_ROTATION_SLOT_MS = 20000;
66
-
67
- // Persistent last-known-good promo record. Lives outside the /tmp cache so it
68
- // survives a full cache wipe / cache write race / CLI failure combo. Written
69
- // every time we successfully render a promo; read as a last resort so the row
70
- // never blinks out mid-session (was: 'promo shows then hides' bug report).
71
- const PROMO_MEMO_FILE = path.join(os.homedir(), '.ruflo', 'statusline-promo.json');
72
- const PROMO_MEMO_TTL_MS = 6 * 60 * 60 * 1000; // 6h β€” long enough to bridge any hiccup, short enough that a real disable takes effect fast.
73
-
74
- // #2337: resolve an already-installed @claude-flow/cli (or ruflo) bin so we
75
- // can invoke it directly via `node`. The previous version called
76
- // `npx --yes @claude-flow/cli@latest` on every uncached render, which forces
77
- // a registry resolution + cold-start of the entire CLI per render. With
78
- // multiple concurrent Claude Code sessions this storms the host (reporter
79
- // saw load average 40-65 on a 12-core box).
80
- //
81
- // Returns EVERY existing bin/cli.js candidate, in preference order (project,
82
- // monorepo, plugin marketplace, global node_modules including custom-prefix
83
- // layouts like ~/.npm-global) β€” mirrors getPkgVersion()'s own path probing.
84
- //
85
- // Returns a list, not a single winner: `fs.existsSync` only proves a file is
86
- // present, not that it actually runs. A marketplace/npx-cached install can
87
- // exist on disk but be broken (observed in practice: a stale marketplace
88
- // checkout whose dist/ imports a workspace package, '@claude-flow/cli-core',
89
- // that isn't bundled there β€” every invocation throws ERR_MODULE_NOT_FOUND).
90
- // Picking the first EXISTING path and never falling through meant a single
91
- // broken install silently killed the promo row for the entire session (the
92
- // CLI call always failed, so the memo could never refresh and eventually
93
- // expired). getStatuslineData() now walks this whole list and tries the next
94
- // candidate on failure, so one broken install can't permanently wedge it.
95
- function resolveCliBinCandidates() {
96
- const candidates = [];
97
- try {
98
- const home = os.homedir();
99
- candidates.push(
100
- path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'bin', 'cli.js'),
101
- path.join(CWD, 'node_modules', '@claude-flow', 'cli', 'bin', 'cli.js'),
102
- path.join(CWD, 'node_modules', 'ruflo', 'bin', 'cli.js'),
103
- path.join(CWD, 'v3', '@claude-flow', 'cli', 'bin', 'cli.js'),
104
- );
105
- try {
106
- const binDir = path.dirname(process.execPath);
107
- const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')];
108
- for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
109
- if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
110
- }
111
- for (const gm of globalModuleDirs) {
112
- candidates.push(
113
- path.join(gm, 'ruflo', 'bin', 'cli.js'),
114
- path.join(gm, '@claude-flow', 'cli', 'bin', 'cli.js'),
115
- );
116
- }
117
- } catch { /* ignore */ }
118
- } catch { /* ignore */ }
119
- return candidates.filter((p) => {
120
- try {
121
- if (!fs.existsSync(p)) return false;
122
- // A candidate's bin/cli.js can exist on disk while its compiled
123
- // dist/ never got built (Claude Code's own plugin marketplace just
124
- // git-clones the repo β€” no install/build step β€” so every marketplace
125
- // install is a source-only checkout by construction). Importing
126
- // dist/src/index.js from bin/cli.js then throws MODULE_NOT_FOUND on
127
- // every real command; only --version happens to survive it. Check
128
- // for the compiled entrypoint too so a doomed candidate is skipped
129
- // up front instead of wasting a spawn-and-fail on every render.
130
- return fs.existsSync(path.join(path.dirname(p), '..', 'dist', 'src', 'index.js'));
131
- } catch { return false; }
132
- });
133
- }
134
-
135
- // Return { fresh, promoFresh, data }. 'fresh' is true only if within the TTL
136
- // β€” but data is returned regardless (stale-while-revalidate). This lets us
137
- // serve last known state (specifically the promo row) when the CLI is
138
- // slow/unavailable, so users don't see the funnel row flicker in and out on
139
- // cache expiry. 'promoFresh' is a SEPARATE, tighter check on the same clock
140
- // as PROMO_ROTATION_SLOT_MS β€” see that constant's comment for why the promo
141
- // row needs its own freshness bound distinct from the general 60s TTL.
142
- function readCache() {
143
- try {
144
- if (fs.existsSync(CACHE_FILE)) {
145
- const raw = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
146
- if (raw && raw._ts && raw.data) {
147
- const age = Date.now() - raw._ts;
148
- return { fresh: age < CACHE_TTL_MS, promoFresh: age < PROMO_ROTATION_SLOT_MS, data: raw.data };
149
- }
150
- }
151
- } catch { /* ignore */ }
152
- return { fresh: false, promoFresh: false, data: null };
153
- }
154
-
155
- function writeCache(data) {
156
- try { fs.writeFileSync(CACHE_FILE, JSON.stringify({ _ts: Date.now(), data }), 'utf-8'); } catch { /* ignore */ }
157
- // Also memoize any promo we saw so the row can survive future CLI hiccups.
158
- try {
159
- if (data && data.promo && typeof data.promo === 'object') {
160
- fs.mkdirSync(path.dirname(PROMO_MEMO_FILE), { recursive: true, mode: 0o700 });
161
- fs.writeFileSync(PROMO_MEMO_FILE, JSON.stringify({ _ts: Date.now(), promo: data.promo }), { encoding: 'utf-8', mode: 0o600 });
162
- }
163
- } catch { /* ignore */ }
164
- }
165
-
166
- // Last resort: read a memoized promo (up to 6h old). Used when no cache and
167
- // no CLI response is available β€” the row still renders, so users don't see
168
- // the disclosure blink out. Returns null when the memo is absent, expired,
169
- // or malformed. Never throws.
170
- function readPromoMemo() {
171
- try {
172
- if (!fs.existsSync(PROMO_MEMO_FILE)) return null;
173
- const raw = JSON.parse(fs.readFileSync(PROMO_MEMO_FILE, 'utf-8'));
174
- if (raw && raw._ts && (Date.now() - raw._ts) < PROMO_MEMO_TTL_MS && raw.promo) {
175
- return raw.promo;
176
- }
177
- } catch { /* ignore */ }
178
- return null;
179
- }
180
-
181
- /**
182
- * Single source of truth: delegate to the CLI hooks statusline --json command.
183
- * Falls back to a minimal static object on failure so the statusline still renders.
184
- *
185
- * Fix for ruflo#2195: the previous local readers returned 0 for AgentDB patterns
186
- * (missed the .swarm/memory.db β†’ AgentDB path), computed dddProgress wrong,
187
- * and only counted ADRs in v3/implementation/adrs/ (missed v3/docs/adr/).
188
- */
189
- // Overlay the memoized promo onto any data object that's missing one. This is
190
- // the safety net that keeps the funnel row rendered when an OLDER cached CLI
191
- // version is picked up by npx β€” that older CLI succeeds but omits promo, so
192
- // the JSON round-trips clean but without our row. We patch it back here.
193
- function overlayMemoPromo(data) {
194
- if (data && !data.promo) {
195
- const memoPromo = readPromoMemo();
196
- if (memoPromo) data.promo = memoPromo;
197
- }
198
- return data;
199
- }
200
-
201
- function getStatuslineData() {
202
- const cache = readCache();
203
- // Both clocks must be satisfied to skip the CLI call entirely: the general
204
- // 60s TTL (#2337 β€” don't re-spawn the CLI on every rapid re-render) AND the
205
- // tighter promo-rotation clock (this fix β€” don't let a still-fresh 60s
206
- // cache silently freeze the promo/insight row across multiple 20s slots).
207
- if (cache.fresh && cache.promoFresh) return overlayMemoPromo(cache.data);
208
-
209
- // #2337: prefer an already-installed CLI bin via direct `node` invocation β€”
210
- // no npx, no registry round-trip, no @latest re-resolve per render. Try
211
- // every candidate that actually EXISTS (not just the first) before falling
212
- // back to `npx --prefer-offline @claude-flow/cli` (no @latest); an existing
213
- // but broken install (e.g. a stale marketplace checkout missing a bundled
214
- // workspace dep) must not block trying the next one.
215
- //
216
- // No `2>/dev/null` here (deliberately) β€” the execSync call below already
217
- // sets stdio: ['pipe','pipe','pipe'], which captures/discards stderr at the
218
- // Node level regardless of shell. The redirect was redundant on POSIX and
219
- // actively broke every candidate on Windows: cmd.exe (execSync's default
220
- // shell there) doesn't understand /dev/null, so the CLI delegation always
221
- // failed, silently degrading every render to buildLocalFallback() β€” 0%
222
- // intelligence and an empty promo row (the memo cache that keeps the row
223
- // populated across CLI hiccups is only ever written from a SUCCESSFUL
224
- // delegation, so it could never get seeded on Windows either).
225
- const cmds = resolveCliBinCandidates()
226
- .map((bin) => '"' + process.execPath + '" "' + bin + '" hooks statusline --json')
227
- .concat(['npx --prefer-offline @claude-flow/cli hooks statusline --json']);
228
- for (const cmd of cmds) {
229
- try {
230
- const raw = execSync(
231
- cmd,
232
- { encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'], cwd: CWD }
233
- ).trim();
234
- // The CLI may emit preamble lines before the JSON β€” find the first '{'.
235
- const jsonStart = raw.indexOf('{');
236
- if (jsonStart === -1) throw new Error('no JSON in CLI output');
237
- const data = JSON.parse(raw.slice(jsonStart));
238
- // Overlay every block the CLI JSON omits (adrs/agentdb/tests/hooks/integration)
239
- // with real local reads, so those segments reflect actual state instead of 0.
240
- applyLocalOverlays(data);
241
- overlayMemoPromo(data);
242
- writeCache(data);
243
- return data;
244
- } catch { /* this candidate unavailable, broken, or timed out β€” try the next */ }
245
- }
246
-
247
- // Stale-while-revalidate: if we have any cached data, keep serving it so the
248
- // funnel row doesn't flicker on CLI hiccups. Overlay fresh local reads for
249
- // the segments the CLI JSON doesn't populate; the promo row survives.
250
- if (cache.data) {
251
- applyLocalOverlays(cache.data);
252
- overlayMemoPromo(cache.data);
253
- return cache.data;
254
- }
255
-
256
- // Last resort: local probes + memo. Users still see the funnel row.
257
- return overlayMemoPromo(buildLocalFallback());
258
- }
259
-
260
- // Count ADRs from BOTH known directories (fix for ruflo#2195: old code missed
261
- // v3/docs/adr/ which holds ADR-088..ADR-137, i.e. 41 of the 128 total ADRs).
262
- function getLocalADRCount() {
263
- const adrDirs = [
264
- path.join(CWD, 'v3', 'implementation', 'adrs'),
265
- path.join(CWD, 'v3', 'docs', 'adr'),
266
- path.join(CWD, 'docs', 'adrs'),
267
- path.join(CWD, '.claude-flow', 'adrs'),
268
- ];
269
- let total = 0;
270
- for (const dir of adrDirs) {
271
- try {
272
- if (fs.existsSync(dir)) {
273
- const files = fs.readdirSync(dir).filter(function(f) {
274
- return f.endsWith('.md') && (f.startsWith('ADR-') || f.startsWith('adr-') || /^\d{4}-/.test(f));
275
- });
276
- total += files.length;
277
- }
278
- } catch { /* ignore */ }
279
- }
280
- return { count: total, implemented: total, compliance: 0 };
281
- }
282
-
283
- // ─── Local overlays for segments the CLI JSON omits ──────────────
284
- // 'hooks statusline --json' only returns user/v3Progress/security/swarm/system.
285
- // agentdb/tests/hooks/integration are never populated, so without these overlays
286
- // they render as a permanent 0. Each reader is cheap and degrades to zeros.
287
-
288
- // Real AgentDB stats from the local memory DB. Vectors live in .swarm/memory.db
289
- // (sql.js + HNSW); ruvector.db is an opaque redb store counted only toward size.
290
- // One read-only sqlite3 query (mode=ro never takes a write lock the daemon owns).
291
- function getLocalAgentDB() {
292
- const result = { vectorCount: 0, dbSizeKB: 0, hasHnsw: false };
293
- try {
294
- let bytes = 0;
295
- for (const f of ['.swarm/memory.db', 'ruvector.db']) {
296
- try { bytes += fs.statSync(path.join(CWD, f)).size; } catch { /* missing */ }
297
- }
298
- result.dbSizeKB = Math.round(bytes / 1024);
299
-
300
- const memDb = path.join(CWD, '.swarm', 'memory.db');
301
- if (fs.existsSync(memDb)) {
302
- const Q = String.fromCharCode(34);
303
- // Two INDEPENDENT statements -- do NOT combine into one. Coupling the
304
- // vector count with the vector_indexes row count in a single statement
305
- // meant that on a DB missing the vector_indexes table (older/agentdb-
306
- // written DBs), the whole statement failed at PREPARE time (SQLite
307
- // compiles the full SQL before running), so the valid memory_entries
308
- // count was discarded too and the statusline showed Vectors 0 despite
309
- // thousands of real vectors. Split so a missing table can only zero the
310
- // HNSW flag, never the count. The init self-heal provisions the table so
311
- // the flag recovers on the next ruflo init / MCP start.
312
- const countSql = Q + 'SELECT COUNT(*) FROM memory_entries WHERE embedding IS NOT NULL;' + Q;
313
- const vc = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + countSql, 1500);
314
- if (vc) result.vectorCount = parseInt(vc, 10) || 0;
315
- // HNSW flag: separate statement. If vector_indexes is absent, sqlite3
316
- // exits non-zero and safeExec returns empty -- hasHnsw stays false (exact
317
- // original semantics: at least one index-config row present).
318
- const hnswSql = Q + 'SELECT COUNT(*) FROM vector_indexes;' + Q;
319
- const hn = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + hnswSql, 1500);
320
- if (hn) result.hasHnsw = (parseInt(hn, 10) || 0) > 0;
321
- }
322
- } catch { /* ignore */ }
323
- return result;
324
- }
325
-
326
- // Count test files via a bounded directory walk (no file reads).
327
- function getLocalTests() {
328
- let testFiles = 0;
329
- function countTests(dir, depth) {
330
- if ((depth || 0) > 4) return;
331
- try {
332
- if (!fs.existsSync(dir)) return;
333
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
334
- if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') {
335
- countTests(path.join(dir, e.name), (depth || 0) + 1);
336
- } else if (e.isFile() && (e.name.includes('.test.') || e.name.includes('.spec.') || e.name.startsWith('test_') || e.name.startsWith('spec_'))) {
337
- testFiles++;
338
- }
339
- }
340
- } catch { /* ignore */ }
341
- }
342
- for (const d of ['tests', 'test', '__tests__', 'src', 'v3']) countTests(path.join(CWD, d));
343
- return { testFiles, testCases: testFiles * 4 };
344
- }
345
-
346
- // Count configured hooks from project .claude/settings.json. Claude Code hooks
347
- // have no enabled/disabled flag, so every configured hook counts as enabled.
348
- function getLocalHooks() {
349
- const result = { enabled: 0, total: 0 };
350
- try {
351
- const settings = readJSON(path.join(CWD, '.claude', 'settings.json'));
352
- const hooks = settings && settings.hooks;
353
- if (hooks && typeof hooks === 'object') {
354
- let n = 0;
355
- for (const ev of Object.keys(hooks)) {
356
- const groups = hooks[ev];
357
- if (Array.isArray(groups)) {
358
- for (const g of groups) {
359
- if (g && Array.isArray(g.hooks)) n += g.hooks.length;
360
- }
361
- }
362
- }
363
- result.total = n;
364
- result.enabled = n;
365
- }
366
- } catch { /* ignore */ }
367
- return result;
368
- }
369
-
370
- // Best-effort integration block: DB presence + locally-configured stdio MCP
371
- // servers (project .mcp.json + global ~/.claude.json). Remote connectors are
372
- // account-managed and not present in local config, so they are not counted.
373
- function getLocalIntegration() {
374
- const integration = { mcpServers: { enabled: 0, total: 0 }, hasDatabase: false };
375
- try {
376
- for (const f of ['.swarm/memory.db', 'ruvector.db']) {
377
- if (fs.existsSync(path.join(CWD, f))) { integration.hasDatabase = true; break; }
378
- }
379
- const names = new Set();
380
- const projMcp = readJSON(path.join(CWD, '.mcp.json'));
381
- if (projMcp && projMcp.mcpServers) for (const k of Object.keys(projMcp.mcpServers)) names.add(k);
382
- const claudeJson = readJSON(path.join(os.homedir(), '.claude.json'));
383
- if (claudeJson) {
384
- if (claudeJson.mcpServers) for (const k of Object.keys(claudeJson.mcpServers)) names.add(k);
385
- const proj = claudeJson.projects && claudeJson.projects[CWD];
386
- if (proj && proj.mcpServers && !Array.isArray(proj.mcpServers)) {
387
- for (const k of Object.keys(proj.mcpServers)) names.add(k);
388
- }
389
- }
390
- integration.mcpServers.total = names.size;
391
- integration.mcpServers.enabled = names.size;
392
- } catch { /* ignore */ }
393
- return integration;
394
- }
395
-
396
- // Overlay every locally-derived block onto the CLI data (mutates in place).
397
- function applyLocalOverlays(data) {
398
- data.adrs = getLocalADRCount();
399
- data.agentdb = getLocalAgentDB();
400
- data.tests = getLocalTests();
401
- data.hooks = getLocalHooks();
402
- data.integration = getLocalIntegration();
403
- return data;
404
- }
405
-
406
- // Minimal local fallback when the CLI is not installed or times out.
407
- // Returns a structure that matches the CLI JSON schema so the renderer works.
408
- function buildLocalFallback() {
409
- const memMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
410
-
411
- return applyLocalOverlays({
412
- user: { name: 'user', gitBranch: '', modelName: 'Claude Code' },
413
- v3Progress: { domainsCompleted: 0, totalDomains: 5, dddProgress: 0, patternsLearned: 0, sessionsCompleted: 0 },
414
- security: { status: 'NONE', cvesFixed: 0, totalCves: 0 },
415
- swarm: { activeAgents: 0, maxAgents: CONFIG.maxAgents, coordinationActive: false },
416
- system: { memoryMB: memMB, contextPct: 0, intelligencePct: 0, subAgents: 0 },
417
- lastUpdated: new Date().toISOString(),
418
- });
419
- }
420
-
421
- // ANSI colors
422
- const c = {
423
- reset: '\x1b[0m',
424
- bold: '\x1b[1m',
425
- dim: '\x1b[2m',
426
- red: '\x1b[0;31m',
427
- green: '\x1b[0;32m',
428
- yellow: '\x1b[0;33m',
429
- blue: '\x1b[0;34m',
430
- purple: '\x1b[0;35m',
431
- cyan: '\x1b[0;36m',
432
- brightRed: '\x1b[1;31m',
433
- brightGreen: '\x1b[1;32m',
434
- brightYellow: '\x1b[1;33m',
435
- brightBlue: '\x1b[1;34m',
436
- brightPurple: '\x1b[1;35m',
437
- brightCyan: '\x1b[1;36m',
438
- brightWhite: '\x1b[1;37m',
439
- };
440
-
441
- // Safe execSync with strict timeout (returns empty string on failure)
442
- function safeExec(cmd, timeoutMs) {
443
- try {
444
- return execSync(cmd, {
445
- encoding: 'utf-8',
446
- timeout: timeoutMs || 2000,
447
- stdio: ['pipe', 'pipe', 'pipe'],
448
- }).trim();
449
- } catch {
450
- return '';
451
- }
452
- }
453
-
454
- // Safe JSON file reader (returns null on failure)
455
- function readJSON(filePath) {
456
- try {
457
- if (fs.existsSync(filePath)) {
458
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
459
- }
460
- } catch { /* ignore */ }
461
- return null;
462
- }
463
-
464
- // ─── Git info (pure-Node / single exec β€” needed for branch display) ──────────
465
-
466
- function getGitInfo() {
467
- const result = {
468
- name: 'user', gitBranch: '', modified: 0, untracked: 0,
469
- staged: 0, ahead: 0, behind: 0,
470
- };
471
-
472
- const script = [
473
- 'git config user.name 2>/dev/null || echo user',
474
- 'echo "---SEP---"',
475
- 'git branch --show-current 2>/dev/null',
476
- 'echo "---SEP---"',
477
- 'git status --porcelain 2>/dev/null',
478
- 'echo "---SEP---"',
479
- 'git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null || echo "0 0"',
480
- ].join('; ');
481
-
482
- const raw = safeExec("sh -c '" + script + "'", 3000);
483
- if (!raw) return result;
484
-
485
- const parts = raw.split('---SEP---').map(function(s) { return s.trim(); });
486
- if (parts.length >= 4) {
487
- result.name = parts[0] || 'user';
488
- result.gitBranch = parts[1] || '';
489
-
490
- if (parts[2]) {
491
- for (const line of parts[2].split('\n')) {
492
- if (!line || line.length < 2) continue;
493
- const x = line[0], y = line[1];
494
- if (x === '?' && y === '?') { result.untracked++; continue; }
495
- if (x !== ' ' && x !== '?') result.staged++;
496
- if (y !== ' ' && y !== '?') result.modified++;
497
- }
498
- }
499
-
500
- const ab = (parts[3] || '0 0').split(/\s+/);
501
- result.ahead = parseInt(ab[0]) || 0;
502
- result.behind = parseInt(ab[1]) || 0;
503
- }
504
-
505
- return result;
506
- }
507
-
508
- // Detect model name from Claude config (pure file reads, no exec)
509
- function getModelName() {
510
- try {
511
- const claudeConfig = readJSON(path.join(os.homedir(), '.claude.json'));
512
- if (claudeConfig && claudeConfig.projects) {
513
- for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
514
- if (CWD === projectPath || CWD.startsWith(projectPath + '/')) {
515
- const usage = projectConfig.lastModelUsage;
516
- if (usage) {
517
- const ids = Object.keys(usage);
518
- if (ids.length > 0) {
519
- let modelId = ids[ids.length - 1];
520
- let latest = 0;
521
- for (const id of ids) {
522
- const ts = usage[id] && usage[id].lastUsedAt ? new Date(usage[id].lastUsedAt).getTime() : 0;
523
- if (ts > latest) { latest = ts; modelId = id; }
524
- }
525
- if (modelId.includes('opus')) return 'Opus 4.8';
526
- if (modelId.includes('sonnet')) return 'Sonnet 4.6';
527
- if (modelId.includes('haiku')) return 'Haiku 4.5';
528
- return modelId.split('-').slice(1, 3).join(' ');
529
- }
530
- }
531
- break;
532
- }
533
- }
534
- }
535
- } catch { /* ignore */ }
536
-
537
- // Fallback: settings.json model field
538
- const settings = getSettings();
539
- if (settings && settings.model) {
540
- const m = settings.model;
541
- if (m.includes('opus')) return 'Opus 4.8';
542
- if (m.includes('sonnet')) return 'Sonnet 4.6';
543
- if (m.includes('haiku')) return 'Haiku 4.5';
544
- }
545
- return 'Claude Code';
546
- }
547
-
548
- // ─── Stdin reader (Claude Code pipes session JSON) ──────────────
549
- // Claude Code sends session JSON via stdin. Read synchronously so the
550
- // script works both when invoked by Claude Code (stdin has JSON) and
551
- // when run manually from terminal (stdin is empty/tty).
552
- let _stdinData = null;
553
- function getStdinData() {
554
- if (_stdinData !== undefined && _stdinData !== null) return _stdinData;
555
- try {
556
- if (process.stdin.isTTY) { _stdinData = null; return null; }
557
- const chunks = [];
558
- const buf = Buffer.alloc(4096);
559
- let bytesRead;
560
- try {
561
- while ((bytesRead = fs.readSync(0, buf, 0, buf.length, null)) > 0) {
562
- chunks.push(buf.slice(0, bytesRead));
563
- }
564
- } catch { /* EOF or read error */ }
565
- const raw = Buffer.concat(chunks).toString('utf-8').trim();
566
- _stdinData = (raw && raw.startsWith('{')) ? JSON.parse(raw) : null;
567
- } catch {
568
- _stdinData = null;
569
- }
570
- return _stdinData;
571
- }
572
-
573
- function getModelFromStdin() {
574
- const data = getStdinData();
575
- return (data && data.model && data.model.display_name) ? data.model.display_name : null;
576
- }
577
-
578
- function getContextFromStdin() {
579
- const data = getStdinData();
580
- if (data && data.context_window) {
581
- return { usedPct: Math.floor(data.context_window.used_percentage || 0) };
582
- }
583
- return null;
584
- }
585
-
586
- function getCostFromStdin() {
587
- const data = getStdinData();
588
- if (data && data.cost) {
589
- const durationMs = data.cost.total_duration_ms || 0;
590
- const mins = Math.floor(durationMs / 60000);
591
- const secs = Math.floor((durationMs % 60000) / 1000);
592
- return {
593
- costUsd: data.cost.total_cost_usd || 0,
594
- duration: mins > 0 ? mins + 'm' + secs + 's' : secs + 's',
595
- };
596
- }
597
- return null;
598
- }
599
-
600
- // Compares dotted-numeric version strings (e.g. "3.27.1" vs "3.27.10").
601
- // Returns >0 if a>b, <0 if a<b, 0 if equal-as-far-as-parseable. Deliberately
602
- // simple (no prerelease/build-metadata handling) β€” this only orders local
603
- // package.json versions against each other, never anything untrusted from
604
- // a payload, so a full semver implementation would be dead weight here.
605
- function compareVersions(a, b) {
606
- const pa = String(a).split('.').map((n) => parseInt(n, 10));
607
- const pb = String(b).split('.').map((n) => parseInt(n, 10));
608
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
609
- const na = Number.isFinite(pa[i]) ? pa[i] : 0;
610
- const nb = Number.isFinite(pb[i]) ? pb[i] : 0;
611
- if (na !== nb) return na - nb;
612
- }
613
- return 0;
614
- }
615
-
616
- function getPkgVersion() {
617
- // Baked in at generation time from the real running CLI's own resolved
618
- // version (see generateStatuslineScript()'s doc comment) β€” correct even
619
- // when this renders via a pure npx invocation with no local install for
620
- // the candidate scan below to find.
621
- let ver = "3.28.0";
622
- try {
623
- const home = os.homedir();
624
- const pkgPaths = [
625
- path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'package.json'),
626
- path.join(CWD, 'node_modules', '@claude-flow', 'cli', 'package.json'),
627
- path.join(CWD, 'node_modules', 'ruflo', 'package.json'),
628
- path.join(CWD, 'v3', '@claude-flow', 'cli', 'package.json'),
629
- ];
630
- // #2221: global installs (npm i -g ruflo) live outside CWD/node_modules, so the
631
- // probes above all miss and the version falls back to the hard-coded default.
632
- // Derive the global node_modules dir from the running node binary (no npm spawn β€”
633
- // statusline renders often). Covers nvm/mise (bin/../lib/node_modules) and Windows
634
- // (bin/node_modules) layouts.
635
- try {
636
- const binDir = path.dirname(process.execPath);
637
- const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')];
638
- // #2221 follow-up: a custom npm prefix (e.g. ~/.npm-global) is decoupled from
639
- // the node binary location, so the binDir-derived probes above all miss. Also
640
- // probe the npm prefix from the environment and the common ~/.npm-global default.
641
- for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
642
- if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
643
- }
644
- for (const gm of globalModuleDirs) {
645
- pkgPaths.push(
646
- path.join(gm, 'ruflo', 'package.json'),
647
- path.join(gm, '@claude-flow', 'cli', 'package.json'),
648
- );
649
- }
650
- } catch { /* ignore */ }
651
- // Pick the HIGHEST version among every candidate that exists, not the
652
- // first one found. The marketplace plugin path is probed first (list
653
- // order above), but Claude Code's own plugin marketplace mechanism
654
- // syncs on its own git-pull cadence, independent of npm publishes β€” a
655
- // freshly-published npm version can sit alongside a stale marketplace
656
- // checkout for a while (observed live: marketplace one release behind
657
- // right after a publish). Taking the first EXISTING candidate meant the
658
- // header could show a stale version even when a newer install (e.g.
659
- // node_modules/@claude-flow/cli from a plain npm install) was sitting right there.
660
- let found = false;
661
- for (const p of pkgPaths) {
662
- if (!fs.existsSync(p)) continue;
663
- try {
664
- const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
665
- if (pkg && typeof pkg.version === 'string' && pkg.version.length > 0) {
666
- if (!found || compareVersions(pkg.version, ver) > 0) ver = pkg.version;
667
- found = true;
668
- }
669
- } catch { /* ignore */ }
670
- }
671
- } catch { /* ignore */ }
672
- return ver;
673
- }
674
-
675
- // ─── Rendering ──────────────────────────────────────────────────
676
-
677
- function progressBar(current, total) {
678
- const width = 5;
679
- const filled = Math.round((current / total) * width);
680
- return '[' + '●'.repeat(filled) + 'β—‹'.repeat(width - filled) + ']';
681
- }
682
-
683
- function generateStatusline() {
684
- const d = getStatuslineData();
685
- const git = getGitInfo();
686
- const modelName = getModelFromStdin() || (d.user && d.user.modelName) || 'Claude Code';
687
- const ctxInfo = getContextFromStdin();
688
- const costInfo = getCostFromStdin();
689
- // Named RUFLO_VERSION (not pkgVersion) so the #1951 regression guard
690
- // (scripts/audit-fix-invariants.mjs) can pin its presence in the emitted
691
- // .cjs artifact β€” without it the header silently reverts to a hard-coded
692
- // "RuFlo V3.5" for anyone whose install doesn't match the first probe path.
693
- const RUFLO_VERSION = getPkgVersion();
694
-
695
- const progress = d.v3Progress || {};
696
- const security = d.security || {};
697
- const swarm = d.swarm || {};
698
- const system = d.system || {};
699
- const adrs = d.adrs || {};
700
- const hooks = d.hooks || {};
701
- const agentdb = d.agentdb || {};
702
- const tests = d.tests || {};
703
-
704
- const domainsCompleted = progress.domainsCompleted || 0;
705
- const totalDomains = progress.totalDomains || 5;
706
- const dddProgress = progress.dddProgress || 0;
707
- const patternsLearned = progress.patternsLearned || 0;
708
- const activeAgents = swarm.activeAgents || 0;
709
- const maxAgents = swarm.maxAgents || CONFIG.maxAgents;
710
- const coordinationActive = swarm.coordinationActive || false;
711
- const intelligencePct = system.intelligencePct || 0;
712
- const memoryMB = system.memoryMB || 0;
713
- const subAgents = system.subAgents || 0;
714
- const cvesFixed = security.cvesFixed || 0;
715
- const totalCves = security.totalCves || 0;
716
- const secStatus = security.status || 'NONE';
717
- const adrCount = adrs.count || 0;
718
- const adrImpl = adrs.implemented || 0;
719
- const hooksEnabled = hooks.enabled || 0;
720
- const hooksTotal = hooks.total || 0;
721
- const vectorCount = agentdb.vectorCount || 0;
722
- const hasHnsw = agentdb.hasHnsw || false;
723
- const dbSizeKB = agentdb.dbSizeKB || 0;
724
- const testFiles = tests.testFiles || 0;
725
- const testCases = tests.testCases || testFiles * 4;
726
-
727
- const lines = [];
728
-
729
- // 3-line design (fits Claude Code's visible statusline area β€” line 4+ gets
730
- // replaced by the system guidance / input prompt line):
731
- // Line 1 β€” Header (RuFlo version Β· git Β· model Β· timing Β· context Β· cost)
732
- // Line 2 β€” Compressed ops (Swarm Β· Hooks Β· 🧠 Β· πŸ’Ύ Β· Health)
733
- // Line 3 β€” Promo / disclosure row (funnel surface, ADR-301)
734
-
735
- // ─── Line 1: header ────────────────────────────────────────────
736
- let header = c.bold + c.brightPurple + 'β–Š RuFlo V' + RUFLO_VERSION + ' ' + c.reset;
737
- header += (coordinationActive ? c.brightCyan : c.dim) + '● ' + c.brightCyan + git.name + c.reset;
738
- if (git.gitBranch) {
739
- header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + c.brightBlue + '⏇ ' + git.gitBranch + c.reset;
740
- const changes = git.modified + git.staged + git.untracked;
741
- if (changes > 0) {
742
- let ind = '';
743
- if (git.staged > 0) ind += c.brightGreen + '+' + git.staged + c.reset;
744
- if (git.modified > 0) ind += c.brightYellow + '~' + git.modified + c.reset;
745
- if (git.untracked > 0) ind += c.dim + '?' + git.untracked + c.reset;
746
- header += ' ' + ind;
747
- }
748
- if (git.ahead > 0) header += ' ' + c.brightGreen + '↑' + git.ahead + c.reset;
749
- if (git.behind > 0) header += ' ' + c.brightRed + '↓' + git.behind + c.reset;
750
- }
751
- header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + c.purple + modelName + c.reset;
752
- const duration = costInfo ? costInfo.duration : '';
753
- if (duration) header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + c.cyan + '⏱ ' + duration + c.reset;
754
- if (ctxInfo && ctxInfo.usedPct > 0) {
755
- const ctxColor = ctxInfo.usedPct >= 90 ? c.brightRed : ctxInfo.usedPct >= 70 ? c.brightYellow : c.brightGreen;
756
- header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + ctxColor + '● ' + ctxInfo.usedPct + '% ctx' + c.reset;
757
- }
758
- if (!CONFIG.hideCost && costInfo && costInfo.costUsd > 0) {
759
- header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + c.brightYellow + CONFIG.costSymbol + costInfo.costUsd.toFixed(2) + c.reset;
760
- }
761
- lines.push(header);
762
-
763
- // ─── Line 2: compressed ops ────────────────────────────────────
764
- // Everything actionable in one dense row. Show only what changes what you
765
- // do next; diagnostic detail moves to `ruflo status --verbose`.
766
- const agentsColor = activeAgents > 0 ? c.brightGreen : c.dim;
767
- const hooksColor = hooksEnabled > 0 ? c.brightGreen : c.dim;
768
- const intellColor = intelligencePct >= 80 ? c.brightGreen : intelligencePct >= 40 ? c.brightYellow : c.dim;
769
- const swarmInd = coordinationActive ? c.brightGreen + 'β—‰' + c.reset + ' ' : c.dim + 'β—‹' + c.reset + ' ';
770
- const cvesClean = totalCves === 0 || cvesFixed === totalCves;
771
- const healthAllGreen = (secStatus === 'CLEAN' || secStatus === 'NONE') && cvesClean;
772
- const opsParts = [];
773
- opsParts.push(c.cyan + 'Swarm ' + swarmInd + agentsColor + activeAgents + c.reset + '/' + c.brightWhite + maxAgents + c.reset);
774
- if (subAgents > 0) opsParts.push(c.brightPurple + 'πŸ‘₯ ' + subAgents + c.reset);
775
- opsParts.push(c.cyan + 'Hooks ' + hooksColor + hooksEnabled + c.reset + '/' + c.brightWhite + hooksTotal + c.reset);
776
- opsParts.push(intellColor + '🧠 ' + intelligencePct + '%' + c.reset);
777
- opsParts.push(c.brightCyan + 'πŸ’Ύ ' + memoryMB + 'MB' + c.reset);
778
- // Health: one glyph when green, terse copy when there's something to act on.
779
- if (healthAllGreen) {
780
- opsParts.push(c.brightGreen + 'πŸ›‘ βœ“' + c.reset);
781
- } else {
782
- if (secStatus === 'PENDING') opsParts.push(c.brightYellow + 'πŸ›‘ scan pending' + c.reset);
783
- else if (secStatus === 'IN_PROGRESS') opsParts.push(c.brightYellow + 'πŸ›‘ scanning…' + c.reset);
784
- else if (secStatus === 'STALE') opsParts.push(c.brightYellow + 'πŸ›‘ scan stale' + c.reset);
785
- else if (secStatus !== 'NONE' && secStatus !== 'CLEAN') opsParts.push(c.brightRed + 'πŸ›‘ ' + secStatus.toLowerCase() + c.reset);
786
- if (totalCves > 0 && cvesFixed < totalCves) {
787
- const unfixed = totalCves - cvesFixed;
788
- opsParts.push(c.brightRed + '⚠ ' + unfixed + ' CVE' + (unfixed === 1 ? '' : 's') + c.reset);
789
- }
790
- }
791
- lines.push(opsParts.join(' ' + c.dim + 'Β·' + c.reset + ' '));
792
-
793
- // ─── Line 3: promo / disclosure / insight ───────────────────────
794
- // Colored by content kind so it reads as *what it is*, not as noise:
795
- // disclosure β†’ brightCyan (announcement / capability link)
796
- // promotional β†’ brightPurple (Cognitum sponsor spot)
797
- // educational β†’ yellow (a tip)
798
- // insight β†’ brightRed (environment/task-aware, local, actionable β€”
799
- // distinct from remote content on purpose)
800
- const promoRow = getPromoRow(d);
801
- if (promoRow) {
802
- const kind = (d && d.promo && d.promo.kind) || 'disclosure';
803
- const promoColor = kind === 'promotional' ? c.brightPurple
804
- : kind === 'educational' ? c.yellow
805
- : kind === 'insight' ? c.brightRed
806
- : c.brightCyan;
807
- lines.push(promoColor + promoRow + c.reset);
808
- }
809
-
810
- // Trailing blank line so Claude Code's input prompt gets breathing room
811
- // instead of butting directly against the last statusline row.
812
- return lines.join('\n') + '\n';
813
- }
814
-
815
- // ─── Funnel promo row (ADR-301) ─────────────────────────────────
816
- // Allowlist for OSC 8 hyperlink targets. Ships in code (not in payload) so
817
- // no message can smuggle a link to an unapproved host.
818
- //
819
- // The final destination hosts (cognitum.one / agentics.org) AND the
820
- // click-redirect host are both allowlisted here: promo.ts routes every
821
- // clickable message through the server-side click-redirect (ADR-311 Β§7)
822
- // so promo_open + geo are captured before the 302 to the real target β€”
823
- // so the OSC 8 link the renderer emits points at the redirect host, not
824
- // the final destination directly.
825
- const PROMO_LINK_HOSTS = new Set([
826
- 'cognitum.one', 'www.cognitum.one', 'docs.cognitum.one',
827
- // agentics.org β€” OSS foundation, distinct sponsor domain. Kept in sync
828
- // with messages.ts ALLOWED_URL_HOSTS.
829
- 'agentics.org', 'www.agentics.org',
830
- // Click-redirect host (funnel.ruv.io once its TLS cert is live; the raw
831
- // Cloud Run hostname is allowlisted too since event-transport.ts /
832
- // message-transport.ts / attribution.ts currently point at it as a TEMP
833
- // fallback while the domain mapping's cert provisions).
834
- 'funnel.ruv.io',
835
- 'cognitum-analytics-63rzcdswba-uc.a.run.app',
836
- ]);
837
-
838
- // Emit OSC 8 hyperlinks unless the environment is known-broken. tmux mangles
839
- // raw OSC 8 (see anthropics/claude-code#27047) β€” opt in via env if wrapped.
840
- function terminalSupportsHyperlinks() {
841
- if (process.env.CI || process.env.GITHUB_ACTIONS) return false;
842
- if (process.env.TERM === 'dumb') return false;
843
- if (/^(0|false|off|no)$/i.test(String(process.env.RUFLO_STATUSLINE_HYPERLINKS || ''))) return false;
844
- if (process.env.TMUX && !process.env.RUFLO_STATUSLINE_HYPERLINKS_TMUX) return false;
845
- return true;
846
- }
847
-
848
- // Wrap a label in an OSC 8 hyperlink escape sequence. Falls back to the raw
849
- // label whenever the URL is not an allowlisted https target, when the terminal
850
- // can't render hyperlinks, or when parsing fails β€” a broken link must never
851
- // leave a raw URL or stray escape in the statusline output.
852
- function safeTerminalLink(label, url) {
853
- if (!terminalSupportsHyperlinks()) return label;
854
- if (typeof url !== 'string' || url.length === 0) return label;
855
- let parsed;
856
- try { parsed = new URL(url); } catch { return label; }
857
- if (parsed.protocol !== 'https:') return label;
858
- if (!PROMO_LINK_HOSTS.has(parsed.hostname)) return label;
859
- const cleanLabel = String(label).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '');
860
- if (cleanLabel.length === 0) return label;
861
- const ESC = '\u001b';
862
- return ESC + ']8;;' + parsed.href + ESC + '\\' + cleanLabel + ESC + ']8;;' + ESC + '\\';
863
- }
864
-
865
- function getPromoRow(d) {
866
- try {
867
- if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
868
- if (/^(0|false|off|no)$/i.test(String(process.env.RUFLO_FUNNEL || ''))) return null;
869
- const promo = d && d.promo;
870
- if (!promo || typeof promo.text !== 'string') return null;
871
- // Strip control chars / ANSI / bidi overrides and hard-cap length β€”
872
- // promo copy is data and must never emit its own terminal sequences.
873
- const text = promo.text
874
- .replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '')
875
- .slice(0, 100)
876
- .trim();
877
- if (text.length === 0) return null;
878
- // Split the label from the trailing "Β· manage: ruflo settings" instruction
879
- // so each part gets styling that matches what it actually IS:
880
- // 1. label β€” OSC 8 hyperlink + underline. A real clickable link.
881
- // 2. "manage:" β€” dim. Just a connector word, no action implied.
882
- // 3. "ruflo settings" β€” bold/bright, NOT underlined. This is a shell
883
- // command the user TYPES, not a link they CLICK β€” a terminal can
884
- // never safely execute a command from a click (that would let any
885
- // server-served message run arbitrary commands), so we deliberately
886
- // avoid the underline/OSC8 cues that imply "clickable". Bold+bright
887
- // instead signals "this is the important bit β€” copy/type it".
888
- // Educational tips have no manage tail and no URL β€” plain text through.
889
- const manageIdx = text.indexOf(' Β· manage: ');
890
- const label = manageIdx > 0 ? text.slice(0, manageIdx) : text;
891
- const manageWord = manageIdx > 0 ? ' Β· manage: ' : '';
892
- const command = manageIdx > 0 ? text.slice(manageIdx + manageWord.length) : '';
893
- const UL_ON = '\u001b[4m';
894
- const UL_OFF = '\u001b[24m';
895
- const DIM_ON = '\u001b[2m';
896
- const DIM_OFF = '\u001b[22m';
897
- const BOLD_ON = '\u001b[1m';
898
- const BOLD_OFF = '\u001b[22m';
899
- const linked = promo.url ? UL_ON + safeTerminalLink(label, promo.url) + UL_OFF : label;
900
- if (!command) return linked;
901
- return linked + DIM_ON + manageWord + DIM_OFF + BOLD_ON + command + BOLD_OFF;
902
- } catch (e) {
903
- return null; // the promo row must never break the statusline
904
- }
905
- }
906
-
907
- // JSON output β€” delegates to CLI for accuracy; caller can use --json flag
908
- function generateJSON() {
909
- const d = getStatuslineData();
910
- const git = getGitInfo();
911
- return Object.assign({}, d, {
912
- user: Object.assign({ name: git.name, gitBranch: git.gitBranch }, d.user || {}),
913
- git: { modified: git.modified, untracked: git.untracked, staged: git.staged, ahead: git.ahead, behind: git.behind },
914
- lastUpdated: new Date().toISOString(),
915
- });
916
- }
917
-
918
- // ─── Main ───────────────────────────────────────────────────────
919
- if (process.argv.includes('--json')) {
920
- console.log(JSON.stringify(generateJSON(), null, 2));
921
- } else if (process.argv.includes('--compact')) {
922
- console.log(JSON.stringify(generateJSON()));
923
- } else {
924
- console.log(generateStatusline());
925
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * RuFlo V3 Statusline β€” delegation build (#2195)
4
+ *
5
+ * Fix for ruvnet/ruflo#2195: the previous version re-implemented all data
6
+ * readers locally using fragile file probes that missed AgentDB patterns,
7
+ * the v3/docs/adr/ ADR directory, and the real vector count.
8
+ *
9
+ * This version delegates to 'npx @claude-flow/cli hooks statusline --json'
10
+ * as the single source of truth. That command queries AgentDB directly,
11
+ * counts ADRs in both directories, and reports the real intelligence pct.
12
+ *
13
+ * ADR counting falls back to local file reads so the display still works
14
+ * without network access (counts both v3/docs/adr/ and v3/implementation/adrs/).
15
+ *
16
+ * Cache: JSON result is cached in /tmp for 10s so rapid prompt triggers
17
+ * (every keystroke in some shells) don't hammer the CLI on every call.
18
+ *
19
+ * Usage: node statusline.cjs [--json] [--compact] [--dashboard]
20
+ */
21
+
22
+ /* eslint-disable @typescript-eslint/no-var-requires */
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const { execSync } = require('child_process');
26
+ const os = require('os');
27
+
28
+ // Configuration
29
+ const CONFIG = {
30
+ maxAgents: 15,
31
+ // Session-cost display. Claude Code's cost.total_cost_usd is a client-side
32
+ // estimate that "may differ from your actual bill" and reads as misleading on
33
+ // subscription plans, where token usage is not billed per dollar. These let
34
+ // each user pick what the segment means to them without changing the default.
35
+ // RUFLO_STATUSLINE_COST_SYMBOL override the leading '$' (e.g. ⚑, €, 🌱);
36
+ // set to an empty string for the number alone.
37
+ // RUFLO_STATUSLINE_HIDE_COST 1/true/yes/on removes the segment entirely.
38
+ costSymbol: process.env.RUFLO_STATUSLINE_COST_SYMBOL ?? '$',
39
+ hideCost: /^(1|true|yes|on)$/i.test(process.env.RUFLO_STATUSLINE_HIDE_COST || ''),
40
+ };
41
+
42
+ const CWD = process.cwd();
43
+
44
+ // ─── Delegation cache ───────────────────────────────────────────
45
+ // Cache the CLI JSON result for 60s so rapid prompt re-renders
46
+ // (Claude Code refreshes the statusline several times a second while
47
+ // streaming) don't re-invoke the CLI each time. #2337: bumped 10s→60s
48
+ // because 10s was far too short for how often Claude Code re-renders.
49
+ const CACHE_FILE = path.join(os.tmpdir(), 'ruflo-statusline-cache-' + require('crypto').createHash('md5').update(CWD).digest('hex').slice(0, 8) + '.json');
50
+ const CACHE_TTL_MS = 60000;
51
+
52
+ // The promo/insight row is designed to rotate on a 20s cadence (funnel/
53
+ // rotation.ts's ROTATION_SLOT_MS / funnel/promo.ts's insight-slot check β€”
54
+ // duplicated here as a bare number since this generated script has no
55
+ // runtime import of the funnel module; keep in sync if that constant ever
56
+ // changes). The rotation slot is only ever (re)computed SERVER-SIDE inside
57
+ // the CLI subprocess this file shells out to β€” so a general 60s data cache
58
+ // (correct and necessary for #2337) silently made that 20s design
59
+ // unreachable: cache.fresh stayed true across 2-3 whole rotation slots,
60
+ // so the row visibly "didn't rotate" (user report). Fix: track promo
61
+ // freshness on its OWN, tighter clock β€” when it lags behind the current
62
+ // slot, fall through to a real CLI call even though the REST of the
63
+ // cached data (security/swarm/system) is still within CACHE_TTL_MS. This
64
+ // does not touch or regress #2337's fix; it only adds a narrower check.
65
+ const PROMO_ROTATION_SLOT_MS = 20000;
66
+
67
+ // Persistent last-known-good promo record. Lives outside the /tmp cache so it
68
+ // survives a full cache wipe / cache write race / CLI failure combo. Written
69
+ // every time we successfully render a promo; read as a last resort so the row
70
+ // never blinks out mid-session (was: 'promo shows then hides' bug report).
71
+ const PROMO_MEMO_FILE = path.join(os.homedir(), '.ruflo', 'statusline-promo.json');
72
+ const PROMO_MEMO_TTL_MS = 6 * 60 * 60 * 1000; // 6h β€” long enough to bridge any hiccup, short enough that a real disable takes effect fast.
73
+
74
+ // #2337: resolve an already-installed @claude-flow/cli (or ruflo) bin so we
75
+ // can invoke it directly via `node`. The previous version called
76
+ // `npx --yes @claude-flow/cli@latest` on every uncached render, which forces
77
+ // a registry resolution + cold-start of the entire CLI per render. With
78
+ // multiple concurrent Claude Code sessions this storms the host (reporter
79
+ // saw load average 40-65 on a 12-core box).
80
+ //
81
+ // Returns EVERY existing bin/cli.js candidate, in preference order (project,
82
+ // monorepo, plugin marketplace, global node_modules including custom-prefix
83
+ // layouts like ~/.npm-global) β€” mirrors getPkgVersion()'s own path probing.
84
+ //
85
+ // Returns a list, not a single winner: `fs.existsSync` only proves a file is
86
+ // present, not that it actually runs. A marketplace/npx-cached install can
87
+ // exist on disk but be broken (observed in practice: a stale marketplace
88
+ // checkout whose dist/ imports a workspace package, '@claude-flow/cli-core',
89
+ // that isn't bundled there β€” every invocation throws ERR_MODULE_NOT_FOUND).
90
+ // Picking the first EXISTING path and never falling through meant a single
91
+ // broken install silently killed the promo row for the entire session (the
92
+ // CLI call always failed, so the memo could never refresh and eventually
93
+ // expired). getStatuslineData() now walks this whole list and tries the next
94
+ // candidate on failure, so one broken install can't permanently wedge it.
95
+ function resolveCliBinCandidates() {
96
+ const candidates = [];
97
+ try {
98
+ const home = os.homedir();
99
+ candidates.push(
100
+ path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'bin', 'cli.js'),
101
+ path.join(CWD, 'node_modules', '@claude-flow', 'cli', 'bin', 'cli.js'),
102
+ path.join(CWD, 'node_modules', 'ruflo', 'bin', 'cli.js'),
103
+ path.join(CWD, 'v3', '@claude-flow', 'cli', 'bin', 'cli.js'),
104
+ );
105
+ try {
106
+ const binDir = path.dirname(process.execPath);
107
+ const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')];
108
+ for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
109
+ if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
110
+ }
111
+ for (const gm of globalModuleDirs) {
112
+ candidates.push(
113
+ path.join(gm, 'ruflo', 'bin', 'cli.js'),
114
+ path.join(gm, '@claude-flow', 'cli', 'bin', 'cli.js'),
115
+ );
116
+ }
117
+ } catch { /* ignore */ }
118
+ } catch { /* ignore */ }
119
+ return candidates.filter((p) => {
120
+ try {
121
+ if (!fs.existsSync(p)) return false;
122
+ // A candidate's bin/cli.js can exist on disk while its compiled
123
+ // dist/ never got built (Claude Code's own plugin marketplace just
124
+ // git-clones the repo β€” no install/build step β€” so every marketplace
125
+ // install is a source-only checkout by construction). Importing
126
+ // dist/src/index.js from bin/cli.js then throws MODULE_NOT_FOUND on
127
+ // every real command; only --version happens to survive it. Check
128
+ // for the compiled entrypoint too so a doomed candidate is skipped
129
+ // up front instead of wasting a spawn-and-fail on every render.
130
+ return fs.existsSync(path.join(path.dirname(p), '..', 'dist', 'src', 'index.js'));
131
+ } catch { return false; }
132
+ });
133
+ }
134
+
135
+ // Return { fresh, promoFresh, data }. 'fresh' is true only if within the TTL
136
+ // β€” but data is returned regardless (stale-while-revalidate). This lets us
137
+ // serve last known state (specifically the promo row) when the CLI is
138
+ // slow/unavailable, so users don't see the funnel row flicker in and out on
139
+ // cache expiry. 'promoFresh' is a SEPARATE, tighter check on the same clock
140
+ // as PROMO_ROTATION_SLOT_MS β€” see that constant's comment for why the promo
141
+ // row needs its own freshness bound distinct from the general 60s TTL.
142
+ function readCache() {
143
+ try {
144
+ if (fs.existsSync(CACHE_FILE)) {
145
+ const raw = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
146
+ if (raw && raw._ts && raw.data) {
147
+ const age = Date.now() - raw._ts;
148
+ return { fresh: age < CACHE_TTL_MS, promoFresh: age < PROMO_ROTATION_SLOT_MS, data: raw.data };
149
+ }
150
+ }
151
+ } catch { /* ignore */ }
152
+ return { fresh: false, promoFresh: false, data: null };
153
+ }
154
+
155
+ function writeCache(data) {
156
+ try { fs.writeFileSync(CACHE_FILE, JSON.stringify({ _ts: Date.now(), data }), 'utf-8'); } catch { /* ignore */ }
157
+ // Also memoize any promo we saw so the row can survive future CLI hiccups.
158
+ try {
159
+ if (data && data.promo && typeof data.promo === 'object') {
160
+ fs.mkdirSync(path.dirname(PROMO_MEMO_FILE), { recursive: true, mode: 0o700 });
161
+ fs.writeFileSync(PROMO_MEMO_FILE, JSON.stringify({ _ts: Date.now(), promo: data.promo }), { encoding: 'utf-8', mode: 0o600 });
162
+ }
163
+ } catch { /* ignore */ }
164
+ }
165
+
166
+ // Last resort: read a memoized promo (up to 6h old). Used when no cache and
167
+ // no CLI response is available β€” the row still renders, so users don't see
168
+ // the disclosure blink out. Returns null when the memo is absent, expired,
169
+ // or malformed. Never throws.
170
+ function readPromoMemo() {
171
+ try {
172
+ if (!fs.existsSync(PROMO_MEMO_FILE)) return null;
173
+ const raw = JSON.parse(fs.readFileSync(PROMO_MEMO_FILE, 'utf-8'));
174
+ if (raw && raw._ts && (Date.now() - raw._ts) < PROMO_MEMO_TTL_MS && raw.promo) {
175
+ return raw.promo;
176
+ }
177
+ } catch { /* ignore */ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Single source of truth: delegate to the CLI hooks statusline --json command.
183
+ * Falls back to a minimal static object on failure so the statusline still renders.
184
+ *
185
+ * Fix for ruflo#2195: the previous local readers returned 0 for AgentDB patterns
186
+ * (missed the .swarm/memory.db β†’ AgentDB path), computed dddProgress wrong,
187
+ * and only counted ADRs in v3/implementation/adrs/ (missed v3/docs/adr/).
188
+ */
189
+ // Overlay the memoized promo onto any data object that's missing one. This is
190
+ // the safety net that keeps the funnel row rendered when an OLDER cached CLI
191
+ // version is picked up by npx β€” that older CLI succeeds but omits promo, so
192
+ // the JSON round-trips clean but without our row. We patch it back here.
193
+ function overlayMemoPromo(data) {
194
+ if (data && !data.promo) {
195
+ const memoPromo = readPromoMemo();
196
+ if (memoPromo) data.promo = memoPromo;
197
+ }
198
+ return data;
199
+ }
200
+
201
+ function getStatuslineData() {
202
+ const cache = readCache();
203
+ // Both clocks must be satisfied to skip the CLI call entirely: the general
204
+ // 60s TTL (#2337 β€” don't re-spawn the CLI on every rapid re-render) AND the
205
+ // tighter promo-rotation clock (this fix β€” don't let a still-fresh 60s
206
+ // cache silently freeze the promo/insight row across multiple 20s slots).
207
+ if (cache.fresh && cache.promoFresh) return overlayMemoPromo(cache.data);
208
+
209
+ // #2337: prefer an already-installed CLI bin via direct `node` invocation β€”
210
+ // no npx, no registry round-trip, no @latest re-resolve per render. Try
211
+ // every candidate that actually EXISTS (not just the first) before falling
212
+ // back to `npx --prefer-offline @claude-flow/cli` (no @latest); an existing
213
+ // but broken install (e.g. a stale marketplace checkout missing a bundled
214
+ // workspace dep) must not block trying the next one.
215
+ //
216
+ // No `2>/dev/null` here (deliberately) β€” the execSync call below already
217
+ // sets stdio: ['pipe','pipe','pipe'], which captures/discards stderr at the
218
+ // Node level regardless of shell. The redirect was redundant on POSIX and
219
+ // actively broke every candidate on Windows: cmd.exe (execSync's default
220
+ // shell there) doesn't understand /dev/null, so the CLI delegation always
221
+ // failed, silently degrading every render to buildLocalFallback() β€” 0%
222
+ // intelligence and an empty promo row (the memo cache that keeps the row
223
+ // populated across CLI hiccups is only ever written from a SUCCESSFUL
224
+ // delegation, so it could never get seeded on Windows either).
225
+ const cmds = resolveCliBinCandidates()
226
+ .map((bin) => '"' + process.execPath + '" "' + bin + '" hooks statusline --json')
227
+ .concat(['npx --prefer-offline @claude-flow/cli hooks statusline --json']);
228
+ for (const cmd of cmds) {
229
+ try {
230
+ const raw = execSync(
231
+ cmd,
232
+ { encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'], cwd: CWD }
233
+ ).trim();
234
+ // The CLI may emit preamble lines before the JSON β€” find the first '{'.
235
+ const jsonStart = raw.indexOf('{');
236
+ if (jsonStart === -1) throw new Error('no JSON in CLI output');
237
+ const data = JSON.parse(raw.slice(jsonStart));
238
+ // Overlay every block the CLI JSON omits (adrs/agentdb/tests/hooks/integration)
239
+ // with real local reads, so those segments reflect actual state instead of 0.
240
+ applyLocalOverlays(data);
241
+ overlayMemoPromo(data);
242
+ writeCache(data);
243
+ return data;
244
+ } catch { /* this candidate unavailable, broken, or timed out β€” try the next */ }
245
+ }
246
+
247
+ // Stale-while-revalidate: if we have any cached data, keep serving it so the
248
+ // funnel row doesn't flicker on CLI hiccups. Overlay fresh local reads for
249
+ // the segments the CLI JSON doesn't populate; the promo row survives.
250
+ if (cache.data) {
251
+ applyLocalOverlays(cache.data);
252
+ overlayMemoPromo(cache.data);
253
+ return cache.data;
254
+ }
255
+
256
+ // Last resort: local probes + memo. Users still see the funnel row.
257
+ return overlayMemoPromo(buildLocalFallback());
258
+ }
259
+
260
+ // Count ADRs from BOTH known directories (fix for ruflo#2195: old code missed
261
+ // v3/docs/adr/ which holds ADR-088..ADR-137, i.e. 41 of the 128 total ADRs).
262
+ function getLocalADRCount() {
263
+ const adrDirs = [
264
+ path.join(CWD, 'v3', 'implementation', 'adrs'),
265
+ path.join(CWD, 'v3', 'docs', 'adr'),
266
+ path.join(CWD, 'docs', 'adrs'),
267
+ path.join(CWD, '.claude-flow', 'adrs'),
268
+ ];
269
+ let total = 0;
270
+ for (const dir of adrDirs) {
271
+ try {
272
+ if (fs.existsSync(dir)) {
273
+ const files = fs.readdirSync(dir).filter(function(f) {
274
+ return f.endsWith('.md') && (f.startsWith('ADR-') || f.startsWith('adr-') || /^\d{4}-/.test(f));
275
+ });
276
+ total += files.length;
277
+ }
278
+ } catch { /* ignore */ }
279
+ }
280
+ return { count: total, implemented: total, compliance: 0 };
281
+ }
282
+
283
+ // ─── Local overlays for segments the CLI JSON omits ──────────────
284
+ // 'hooks statusline --json' only returns user/v3Progress/security/swarm/system.
285
+ // agentdb/tests/hooks/integration are never populated, so without these overlays
286
+ // they render as a permanent 0. Each reader is cheap and degrades to zeros.
287
+
288
+ // Real AgentDB stats from the local memory DB. Vectors live in .swarm/memory.db
289
+ // (sql.js + HNSW); ruvector.db is an opaque redb store counted only toward size.
290
+ // One read-only sqlite3 query (mode=ro never takes a write lock the daemon owns).
291
+ function getLocalAgentDB() {
292
+ const result = { vectorCount: 0, dbSizeKB: 0, hasHnsw: false };
293
+ try {
294
+ let bytes = 0;
295
+ for (const f of ['.swarm/memory.db', 'ruvector.db']) {
296
+ try { bytes += fs.statSync(path.join(CWD, f)).size; } catch { /* missing */ }
297
+ }
298
+ result.dbSizeKB = Math.round(bytes / 1024);
299
+
300
+ const memDb = path.join(CWD, '.swarm', 'memory.db');
301
+ if (fs.existsSync(memDb)) {
302
+ const Q = String.fromCharCode(34);
303
+ // Two INDEPENDENT statements -- do NOT combine into one. Coupling the
304
+ // vector count with the vector_indexes row count in a single statement
305
+ // meant that on a DB missing the vector_indexes table (older/agentdb-
306
+ // written DBs), the whole statement failed at PREPARE time (SQLite
307
+ // compiles the full SQL before running), so the valid memory_entries
308
+ // count was discarded too and the statusline showed Vectors 0 despite
309
+ // thousands of real vectors. Split so a missing table can only zero the
310
+ // HNSW flag, never the count. The init self-heal provisions the table so
311
+ // the flag recovers on the next ruflo init / MCP start.
312
+ const countSql = Q + 'SELECT COUNT(*) FROM memory_entries WHERE embedding IS NOT NULL;' + Q;
313
+ const vc = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + countSql, 1500);
314
+ if (vc) result.vectorCount = parseInt(vc, 10) || 0;
315
+ // HNSW flag: separate statement. If vector_indexes is absent, sqlite3
316
+ // exits non-zero and safeExec returns empty -- hasHnsw stays false (exact
317
+ // original semantics: at least one index-config row present).
318
+ const hnswSql = Q + 'SELECT COUNT(*) FROM vector_indexes;' + Q;
319
+ const hn = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + hnswSql, 1500);
320
+ if (hn) result.hasHnsw = (parseInt(hn, 10) || 0) > 0;
321
+ }
322
+ } catch { /* ignore */ }
323
+ return result;
324
+ }
325
+
326
+ // Count test files via a bounded directory walk (no file reads).
327
+ function getLocalTests() {
328
+ let testFiles = 0;
329
+ function countTests(dir, depth) {
330
+ if ((depth || 0) > 4) return;
331
+ try {
332
+ if (!fs.existsSync(dir)) return;
333
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
334
+ if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') {
335
+ countTests(path.join(dir, e.name), (depth || 0) + 1);
336
+ } else if (e.isFile() && (e.name.includes('.test.') || e.name.includes('.spec.') || e.name.startsWith('test_') || e.name.startsWith('spec_'))) {
337
+ testFiles++;
338
+ }
339
+ }
340
+ } catch { /* ignore */ }
341
+ }
342
+ for (const d of ['tests', 'test', '__tests__', 'src', 'v3']) countTests(path.join(CWD, d));
343
+ return { testFiles, testCases: testFiles * 4 };
344
+ }
345
+
346
+ // Count configured hooks from project .claude/settings.json. Claude Code hooks
347
+ // have no enabled/disabled flag, so every configured hook counts as enabled.
348
+ function getLocalHooks() {
349
+ const result = { enabled: 0, total: 0 };
350
+ try {
351
+ const settings = readJSON(path.join(CWD, '.claude', 'settings.json'));
352
+ const hooks = settings && settings.hooks;
353
+ if (hooks && typeof hooks === 'object') {
354
+ let n = 0;
355
+ for (const ev of Object.keys(hooks)) {
356
+ const groups = hooks[ev];
357
+ if (Array.isArray(groups)) {
358
+ for (const g of groups) {
359
+ if (g && Array.isArray(g.hooks)) n += g.hooks.length;
360
+ }
361
+ }
362
+ }
363
+ result.total = n;
364
+ result.enabled = n;
365
+ }
366
+ } catch { /* ignore */ }
367
+ return result;
368
+ }
369
+
370
+ // Best-effort integration block: DB presence + locally-configured stdio MCP
371
+ // servers (project .mcp.json + global ~/.claude.json). Remote connectors are
372
+ // account-managed and not present in local config, so they are not counted.
373
+ function getLocalIntegration() {
374
+ const integration = { mcpServers: { enabled: 0, total: 0 }, hasDatabase: false };
375
+ try {
376
+ for (const f of ['.swarm/memory.db', 'ruvector.db']) {
377
+ if (fs.existsSync(path.join(CWD, f))) { integration.hasDatabase = true; break; }
378
+ }
379
+ const names = new Set();
380
+ const projMcp = readJSON(path.join(CWD, '.mcp.json'));
381
+ if (projMcp && projMcp.mcpServers) for (const k of Object.keys(projMcp.mcpServers)) names.add(k);
382
+ const claudeJson = readJSON(path.join(os.homedir(), '.claude.json'));
383
+ if (claudeJson) {
384
+ if (claudeJson.mcpServers) for (const k of Object.keys(claudeJson.mcpServers)) names.add(k);
385
+ const proj = claudeJson.projects && claudeJson.projects[CWD];
386
+ if (proj && proj.mcpServers && !Array.isArray(proj.mcpServers)) {
387
+ for (const k of Object.keys(proj.mcpServers)) names.add(k);
388
+ }
389
+ }
390
+ integration.mcpServers.total = names.size;
391
+ integration.mcpServers.enabled = names.size;
392
+ } catch { /* ignore */ }
393
+ return integration;
394
+ }
395
+
396
+ // Overlay every locally-derived block onto the CLI data (mutates in place).
397
+ function applyLocalOverlays(data) {
398
+ data.adrs = getLocalADRCount();
399
+ data.agentdb = getLocalAgentDB();
400
+ data.tests = getLocalTests();
401
+ data.hooks = getLocalHooks();
402
+ data.integration = getLocalIntegration();
403
+ return data;
404
+ }
405
+
406
+ // Minimal local fallback when the CLI is not installed or times out.
407
+ // Returns a structure that matches the CLI JSON schema so the renderer works.
408
+ function buildLocalFallback() {
409
+ const memMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
410
+
411
+ return applyLocalOverlays({
412
+ user: { name: 'user', gitBranch: '', modelName: 'Claude Code' },
413
+ v3Progress: { domainsCompleted: 0, totalDomains: 5, dddProgress: 0, patternsLearned: 0, sessionsCompleted: 0 },
414
+ security: { status: 'NONE', cvesFixed: 0, totalCves: 0 },
415
+ swarm: { activeAgents: 0, maxAgents: CONFIG.maxAgents, coordinationActive: false },
416
+ system: { memoryMB: memMB, contextPct: 0, intelligencePct: 0, subAgents: 0 },
417
+ lastUpdated: new Date().toISOString(),
418
+ });
419
+ }
420
+
421
+ // ANSI colors
422
+ const c = {
423
+ reset: '\x1b[0m',
424
+ bold: '\x1b[1m',
425
+ dim: '\x1b[2m',
426
+ red: '\x1b[0;31m',
427
+ green: '\x1b[0;32m',
428
+ yellow: '\x1b[0;33m',
429
+ blue: '\x1b[0;34m',
430
+ purple: '\x1b[0;35m',
431
+ cyan: '\x1b[0;36m',
432
+ brightRed: '\x1b[1;31m',
433
+ brightGreen: '\x1b[1;32m',
434
+ brightYellow: '\x1b[1;33m',
435
+ brightBlue: '\x1b[1;34m',
436
+ brightPurple: '\x1b[1;35m',
437
+ brightCyan: '\x1b[1;36m',
438
+ brightWhite: '\x1b[1;37m',
439
+ };
440
+
441
+ // Safe execSync with strict timeout (returns empty string on failure)
442
+ function safeExec(cmd, timeoutMs) {
443
+ try {
444
+ return execSync(cmd, {
445
+ encoding: 'utf-8',
446
+ timeout: timeoutMs || 2000,
447
+ stdio: ['pipe', 'pipe', 'pipe'],
448
+ }).trim();
449
+ } catch {
450
+ return '';
451
+ }
452
+ }
453
+
454
+ // Safe JSON file reader (returns null on failure)
455
+ function readJSON(filePath) {
456
+ try {
457
+ if (fs.existsSync(filePath)) {
458
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
459
+ }
460
+ } catch { /* ignore */ }
461
+ return null;
462
+ }
463
+
464
+ // ─── Git info (pure-Node / single exec β€” needed for branch display) ──────────
465
+
466
+ function getGitInfo() {
467
+ const result = {
468
+ name: 'user', gitBranch: '', modified: 0, untracked: 0,
469
+ staged: 0, ahead: 0, behind: 0,
470
+ };
471
+
472
+ const script = [
473
+ 'git config user.name 2>/dev/null || echo user',
474
+ 'echo "---SEP---"',
475
+ 'git branch --show-current 2>/dev/null',
476
+ 'echo "---SEP---"',
477
+ 'git status --porcelain 2>/dev/null',
478
+ 'echo "---SEP---"',
479
+ 'git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null || echo "0 0"',
480
+ ].join('; ');
481
+
482
+ const raw = safeExec("sh -c '" + script + "'", 3000);
483
+ if (!raw) return result;
484
+
485
+ const parts = raw.split('---SEP---').map(function(s) { return s.trim(); });
486
+ if (parts.length >= 4) {
487
+ result.name = parts[0] || 'user';
488
+ result.gitBranch = parts[1] || '';
489
+
490
+ if (parts[2]) {
491
+ for (const line of parts[2].split('\n')) {
492
+ if (!line || line.length < 2) continue;
493
+ const x = line[0], y = line[1];
494
+ if (x === '?' && y === '?') { result.untracked++; continue; }
495
+ if (x !== ' ' && x !== '?') result.staged++;
496
+ if (y !== ' ' && y !== '?') result.modified++;
497
+ }
498
+ }
499
+
500
+ const ab = (parts[3] || '0 0').split(/\s+/);
501
+ result.ahead = parseInt(ab[0]) || 0;
502
+ result.behind = parseInt(ab[1]) || 0;
503
+ }
504
+
505
+ return result;
506
+ }
507
+
508
+ // Detect model name from Claude config (pure file reads, no exec)
509
+ function getModelName() {
510
+ try {
511
+ const claudeConfig = readJSON(path.join(os.homedir(), '.claude.json'));
512
+ if (claudeConfig && claudeConfig.projects) {
513
+ for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
514
+ if (CWD === projectPath || CWD.startsWith(projectPath + '/')) {
515
+ const usage = projectConfig.lastModelUsage;
516
+ if (usage) {
517
+ const ids = Object.keys(usage);
518
+ if (ids.length > 0) {
519
+ let modelId = ids[ids.length - 1];
520
+ let latest = 0;
521
+ for (const id of ids) {
522
+ const ts = usage[id] && usage[id].lastUsedAt ? new Date(usage[id].lastUsedAt).getTime() : 0;
523
+ if (ts > latest) { latest = ts; modelId = id; }
524
+ }
525
+ if (modelId.includes('opus')) return 'Opus 4.8';
526
+ if (modelId.includes('sonnet')) return 'Sonnet 4.6';
527
+ if (modelId.includes('haiku')) return 'Haiku 4.5';
528
+ return modelId.split('-').slice(1, 3).join(' ');
529
+ }
530
+ }
531
+ break;
532
+ }
533
+ }
534
+ }
535
+ } catch { /* ignore */ }
536
+
537
+ // Fallback: settings.json model field
538
+ const settings = getSettings();
539
+ if (settings && settings.model) {
540
+ const m = settings.model;
541
+ if (m.includes('opus')) return 'Opus 4.8';
542
+ if (m.includes('sonnet')) return 'Sonnet 4.6';
543
+ if (m.includes('haiku')) return 'Haiku 4.5';
544
+ }
545
+ return 'Claude Code';
546
+ }
547
+
548
+ // ─── Stdin reader (Claude Code pipes session JSON) ──────────────
549
+ // Claude Code sends session JSON via stdin. Read synchronously so the
550
+ // script works both when invoked by Claude Code (stdin has JSON) and
551
+ // when run manually from terminal (stdin is empty/tty).
552
+ let _stdinData = null;
553
+ function getStdinData() {
554
+ if (_stdinData !== undefined && _stdinData !== null) return _stdinData;
555
+ try {
556
+ if (process.stdin.isTTY) { _stdinData = null; return null; }
557
+ const chunks = [];
558
+ const buf = Buffer.alloc(4096);
559
+ let bytesRead;
560
+ try {
561
+ while ((bytesRead = fs.readSync(0, buf, 0, buf.length, null)) > 0) {
562
+ chunks.push(buf.slice(0, bytesRead));
563
+ }
564
+ } catch { /* EOF or read error */ }
565
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
566
+ _stdinData = (raw && raw.startsWith('{')) ? JSON.parse(raw) : null;
567
+ } catch {
568
+ _stdinData = null;
569
+ }
570
+ return _stdinData;
571
+ }
572
+
573
+ function getModelFromStdin() {
574
+ const data = getStdinData();
575
+ return (data && data.model && data.model.display_name) ? data.model.display_name : null;
576
+ }
577
+
578
+ function getContextFromStdin() {
579
+ const data = getStdinData();
580
+ if (data && data.context_window) {
581
+ return { usedPct: Math.floor(data.context_window.used_percentage || 0) };
582
+ }
583
+ return null;
584
+ }
585
+
586
+ function getCostFromStdin() {
587
+ const data = getStdinData();
588
+ if (data && data.cost) {
589
+ const durationMs = data.cost.total_duration_ms || 0;
590
+ const mins = Math.floor(durationMs / 60000);
591
+ const secs = Math.floor((durationMs % 60000) / 1000);
592
+ return {
593
+ costUsd: data.cost.total_cost_usd || 0,
594
+ duration: mins > 0 ? mins + 'm' + secs + 's' : secs + 's',
595
+ };
596
+ }
597
+ return null;
598
+ }
599
+
600
+ // Compares dotted-numeric version strings (e.g. "3.27.1" vs "3.27.10").
601
+ // Returns >0 if a>b, <0 if a<b, 0 if equal-as-far-as-parseable. Deliberately
602
+ // simple (no prerelease/build-metadata handling) β€” this only orders local
603
+ // package.json versions against each other, never anything untrusted from
604
+ // a payload, so a full semver implementation would be dead weight here.
605
+ function compareVersions(a, b) {
606
+ const pa = String(a).split('.').map((n) => parseInt(n, 10));
607
+ const pb = String(b).split('.').map((n) => parseInt(n, 10));
608
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
609
+ const na = Number.isFinite(pa[i]) ? pa[i] : 0;
610
+ const nb = Number.isFinite(pb[i]) ? pb[i] : 0;
611
+ if (na !== nb) return na - nb;
612
+ }
613
+ return 0;
614
+ }
615
+
616
+ function getPkgVersion() {
617
+ // Baked in at generation time from the real running CLI's own resolved
618
+ // version (see generateStatuslineScript()'s doc comment) β€” correct even
619
+ // when this renders via a pure npx invocation with no local install for
620
+ // the candidate scan below to find.
621
+ let ver = "3.28.0";
622
+ try {
623
+ const home = os.homedir();
624
+ const pkgPaths = [
625
+ path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'package.json'),
626
+ path.join(CWD, 'node_modules', '@claude-flow', 'cli', 'package.json'),
627
+ path.join(CWD, 'node_modules', 'ruflo', 'package.json'),
628
+ path.join(CWD, 'v3', '@claude-flow', 'cli', 'package.json'),
629
+ ];
630
+ // #2221: global installs (npm i -g ruflo) live outside CWD/node_modules, so the
631
+ // probes above all miss and the version falls back to the hard-coded default.
632
+ // Derive the global node_modules dir from the running node binary (no npm spawn β€”
633
+ // statusline renders often). Covers nvm/mise (bin/../lib/node_modules) and Windows
634
+ // (bin/node_modules) layouts.
635
+ try {
636
+ const binDir = path.dirname(process.execPath);
637
+ const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')];
638
+ // #2221 follow-up: a custom npm prefix (e.g. ~/.npm-global) is decoupled from
639
+ // the node binary location, so the binDir-derived probes above all miss. Also
640
+ // probe the npm prefix from the environment and the common ~/.npm-global default.
641
+ for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
642
+ if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
643
+ }
644
+ for (const gm of globalModuleDirs) {
645
+ pkgPaths.push(
646
+ path.join(gm, 'ruflo', 'package.json'),
647
+ path.join(gm, '@claude-flow', 'cli', 'package.json'),
648
+ );
649
+ }
650
+ } catch { /* ignore */ }
651
+ // Pick the HIGHEST version among every candidate that exists, not the
652
+ // first one found. The marketplace plugin path is probed first (list
653
+ // order above), but Claude Code's own plugin marketplace mechanism
654
+ // syncs on its own git-pull cadence, independent of npm publishes β€” a
655
+ // freshly-published npm version can sit alongside a stale marketplace
656
+ // checkout for a while (observed live: marketplace one release behind
657
+ // right after a publish). Taking the first EXISTING candidate meant the
658
+ // header could show a stale version even when a newer install (e.g.
659
+ // node_modules/@claude-flow/cli from a plain npm install) was sitting right there.
660
+ let found = false;
661
+ for (const p of pkgPaths) {
662
+ if (!fs.existsSync(p)) continue;
663
+ try {
664
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
665
+ if (pkg && typeof pkg.version === 'string' && pkg.version.length > 0) {
666
+ if (!found || compareVersions(pkg.version, ver) > 0) ver = pkg.version;
667
+ found = true;
668
+ }
669
+ } catch { /* ignore */ }
670
+ }
671
+ } catch { /* ignore */ }
672
+ return ver;
673
+ }
674
+
675
+ // ─── Rendering ──────────────────────────────────────────────────
676
+
677
+ function progressBar(current, total) {
678
+ const width = 5;
679
+ const filled = Math.round((current / total) * width);
680
+ return '[' + '●'.repeat(filled) + 'β—‹'.repeat(width - filled) + ']';
681
+ }
682
+
683
+ function generateStatusline() {
684
+ const d = getStatuslineData();
685
+ const git = getGitInfo();
686
+ const modelName = getModelFromStdin() || (d.user && d.user.modelName) || 'Claude Code';
687
+ const ctxInfo = getContextFromStdin();
688
+ const costInfo = getCostFromStdin();
689
+ // Named RUFLO_VERSION (not pkgVersion) so the #1951 regression guard
690
+ // (scripts/audit-fix-invariants.mjs) can pin its presence in the emitted
691
+ // .cjs artifact β€” without it the header silently reverts to a hard-coded
692
+ // "RuFlo V3.5" for anyone whose install doesn't match the first probe path.
693
+ const RUFLO_VERSION = getPkgVersion();
694
+
695
+ const progress = d.v3Progress || {};
696
+ const security = d.security || {};
697
+ const swarm = d.swarm || {};
698
+ const system = d.system || {};
699
+ const adrs = d.adrs || {};
700
+ const hooks = d.hooks || {};
701
+ const agentdb = d.agentdb || {};
702
+ const tests = d.tests || {};
703
+
704
+ const domainsCompleted = progress.domainsCompleted || 0;
705
+ const totalDomains = progress.totalDomains || 5;
706
+ const dddProgress = progress.dddProgress || 0;
707
+ const patternsLearned = progress.patternsLearned || 0;
708
+ const activeAgents = swarm.activeAgents || 0;
709
+ const maxAgents = swarm.maxAgents || CONFIG.maxAgents;
710
+ const coordinationActive = swarm.coordinationActive || false;
711
+ const intelligencePct = system.intelligencePct || 0;
712
+ const memoryMB = system.memoryMB || 0;
713
+ const subAgents = system.subAgents || 0;
714
+ const cvesFixed = security.cvesFixed || 0;
715
+ const totalCves = security.totalCves || 0;
716
+ const secStatus = security.status || 'NONE';
717
+ const adrCount = adrs.count || 0;
718
+ const adrImpl = adrs.implemented || 0;
719
+ const hooksEnabled = hooks.enabled || 0;
720
+ const hooksTotal = hooks.total || 0;
721
+ const vectorCount = agentdb.vectorCount || 0;
722
+ const hasHnsw = agentdb.hasHnsw || false;
723
+ const dbSizeKB = agentdb.dbSizeKB || 0;
724
+ const testFiles = tests.testFiles || 0;
725
+ const testCases = tests.testCases || testFiles * 4;
726
+
727
+ const lines = [];
728
+
729
+ // 3-line design (fits Claude Code's visible statusline area β€” line 4+ gets
730
+ // replaced by the system guidance / input prompt line):
731
+ // Line 1 β€” Header (RuFlo version Β· git Β· model Β· timing Β· context Β· cost)
732
+ // Line 2 β€” Compressed ops (Swarm Β· Hooks Β· 🧠 Β· πŸ’Ύ Β· Health)
733
+ // Line 3 β€” Promo / disclosure row (funnel surface, ADR-301)
734
+
735
+ // ─── Line 1: header ────────────────────────────────────────────
736
+ let header = c.bold + c.brightPurple + 'β–Š RuFlo V' + RUFLO_VERSION + ' ' + c.reset;
737
+ header += (coordinationActive ? c.brightCyan : c.dim) + '● ' + c.brightCyan + git.name + c.reset;
738
+ if (git.gitBranch) {
739
+ header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + c.brightBlue + '⏇ ' + git.gitBranch + c.reset;
740
+ const changes = git.modified + git.staged + git.untracked;
741
+ if (changes > 0) {
742
+ let ind = '';
743
+ if (git.staged > 0) ind += c.brightGreen + '+' + git.staged + c.reset;
744
+ if (git.modified > 0) ind += c.brightYellow + '~' + git.modified + c.reset;
745
+ if (git.untracked > 0) ind += c.dim + '?' + git.untracked + c.reset;
746
+ header += ' ' + ind;
747
+ }
748
+ if (git.ahead > 0) header += ' ' + c.brightGreen + '↑' + git.ahead + c.reset;
749
+ if (git.behind > 0) header += ' ' + c.brightRed + '↓' + git.behind + c.reset;
750
+ }
751
+ header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + c.purple + modelName + c.reset;
752
+ const duration = costInfo ? costInfo.duration : '';
753
+ if (duration) header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + c.cyan + '⏱ ' + duration + c.reset;
754
+ if (ctxInfo && ctxInfo.usedPct > 0) {
755
+ const ctxColor = ctxInfo.usedPct >= 90 ? c.brightRed : ctxInfo.usedPct >= 70 ? c.brightYellow : c.brightGreen;
756
+ header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + ctxColor + '● ' + ctxInfo.usedPct + '% ctx' + c.reset;
757
+ }
758
+ if (!CONFIG.hideCost && costInfo && costInfo.costUsd > 0) {
759
+ header += ' ' + c.dim + 'β”‚' + c.reset + ' ' + c.brightYellow + CONFIG.costSymbol + costInfo.costUsd.toFixed(2) + c.reset;
760
+ }
761
+ lines.push(header);
762
+
763
+ // ─── Line 2: compressed ops ────────────────────────────────────
764
+ // Everything actionable in one dense row. Show only what changes what you
765
+ // do next; diagnostic detail moves to `ruflo status --verbose`.
766
+ const agentsColor = activeAgents > 0 ? c.brightGreen : c.dim;
767
+ const hooksColor = hooksEnabled > 0 ? c.brightGreen : c.dim;
768
+ const intellColor = intelligencePct >= 80 ? c.brightGreen : intelligencePct >= 40 ? c.brightYellow : c.dim;
769
+ const swarmInd = coordinationActive ? c.brightGreen + 'β—‰' + c.reset + ' ' : c.dim + 'β—‹' + c.reset + ' ';
770
+ const cvesClean = totalCves === 0 || cvesFixed === totalCves;
771
+ const healthAllGreen = (secStatus === 'CLEAN' || secStatus === 'NONE') && cvesClean;
772
+ const opsParts = [];
773
+ opsParts.push(c.cyan + 'Swarm ' + swarmInd + agentsColor + activeAgents + c.reset + '/' + c.brightWhite + maxAgents + c.reset);
774
+ if (subAgents > 0) opsParts.push(c.brightPurple + 'πŸ‘₯ ' + subAgents + c.reset);
775
+ opsParts.push(c.cyan + 'Hooks ' + hooksColor + hooksEnabled + c.reset + '/' + c.brightWhite + hooksTotal + c.reset);
776
+ opsParts.push(intellColor + '🧠 ' + intelligencePct + '%' + c.reset);
777
+ opsParts.push(c.brightCyan + 'πŸ’Ύ ' + memoryMB + 'MB' + c.reset);
778
+ // Health: one glyph when green, terse copy when there's something to act on.
779
+ if (healthAllGreen) {
780
+ opsParts.push(c.brightGreen + 'πŸ›‘ βœ“' + c.reset);
781
+ } else {
782
+ if (secStatus === 'PENDING') opsParts.push(c.brightYellow + 'πŸ›‘ scan pending' + c.reset);
783
+ else if (secStatus === 'IN_PROGRESS') opsParts.push(c.brightYellow + 'πŸ›‘ scanning…' + c.reset);
784
+ else if (secStatus === 'STALE') opsParts.push(c.brightYellow + 'πŸ›‘ scan stale' + c.reset);
785
+ else if (secStatus !== 'NONE' && secStatus !== 'CLEAN') opsParts.push(c.brightRed + 'πŸ›‘ ' + secStatus.toLowerCase() + c.reset);
786
+ if (totalCves > 0 && cvesFixed < totalCves) {
787
+ const unfixed = totalCves - cvesFixed;
788
+ opsParts.push(c.brightRed + '⚠ ' + unfixed + ' CVE' + (unfixed === 1 ? '' : 's') + c.reset);
789
+ }
790
+ }
791
+ lines.push(opsParts.join(' ' + c.dim + 'Β·' + c.reset + ' '));
792
+
793
+ // ─── Line 3: promo / disclosure / insight ───────────────────────
794
+ // Colored by content kind so it reads as *what it is*, not as noise:
795
+ // disclosure β†’ brightCyan (announcement / capability link)
796
+ // promotional β†’ brightPurple (Cognitum sponsor spot)
797
+ // educational β†’ yellow (a tip)
798
+ // insight β†’ brightRed (environment/task-aware, local, actionable β€”
799
+ // distinct from remote content on purpose)
800
+ const promoRow = getPromoRow(d);
801
+ if (promoRow) {
802
+ const kind = (d && d.promo && d.promo.kind) || 'disclosure';
803
+ const promoColor = kind === 'promotional' ? c.brightPurple
804
+ : kind === 'educational' ? c.yellow
805
+ : kind === 'insight' ? c.brightRed
806
+ : c.brightCyan;
807
+ lines.push(promoColor + promoRow + c.reset);
808
+ }
809
+
810
+ // Trailing blank line so Claude Code's input prompt gets breathing room
811
+ // instead of butting directly against the last statusline row.
812
+ return lines.join('\n') + '\n';
813
+ }
814
+
815
+ // ─── Funnel promo row (ADR-301) ─────────────────────────────────
816
+ // Allowlist for OSC 8 hyperlink targets. Ships in code (not in payload) so
817
+ // no message can smuggle a link to an unapproved host.
818
+ //
819
+ // The final destination hosts (cognitum.one / agentics.org) AND the
820
+ // click-redirect host are both allowlisted here: promo.ts routes every
821
+ // clickable message through the server-side click-redirect (ADR-311 Β§7)
822
+ // so promo_open + geo are captured before the 302 to the real target β€”
823
+ // so the OSC 8 link the renderer emits points at the redirect host, not
824
+ // the final destination directly.
825
+ const PROMO_LINK_HOSTS = new Set([
826
+ 'cognitum.one', 'www.cognitum.one', 'docs.cognitum.one',
827
+ // agentics.org β€” OSS foundation, distinct sponsor domain. Kept in sync
828
+ // with messages.ts ALLOWED_URL_HOSTS.
829
+ 'agentics.org', 'www.agentics.org',
830
+ // Click-redirect host (funnel.ruv.io once its TLS cert is live; the raw
831
+ // Cloud Run hostname is allowlisted too since event-transport.ts /
832
+ // message-transport.ts / attribution.ts currently point at it as a TEMP
833
+ // fallback while the domain mapping's cert provisions).
834
+ 'funnel.ruv.io',
835
+ 'cognitum-analytics-63rzcdswba-uc.a.run.app',
836
+ ]);
837
+
838
+ // Emit OSC 8 hyperlinks unless the environment is known-broken. tmux mangles
839
+ // raw OSC 8 (see anthropics/claude-code#27047) β€” opt in via env if wrapped.
840
+ function terminalSupportsHyperlinks() {
841
+ if (process.env.CI || process.env.GITHUB_ACTIONS) return false;
842
+ if (process.env.TERM === 'dumb') return false;
843
+ if (/^(0|false|off|no)$/i.test(String(process.env.RUFLO_STATUSLINE_HYPERLINKS || ''))) return false;
844
+ if (process.env.TMUX && !process.env.RUFLO_STATUSLINE_HYPERLINKS_TMUX) return false;
845
+ return true;
846
+ }
847
+
848
+ // Wrap a label in an OSC 8 hyperlink escape sequence. Falls back to the raw
849
+ // label whenever the URL is not an allowlisted https target, when the terminal
850
+ // can't render hyperlinks, or when parsing fails β€” a broken link must never
851
+ // leave a raw URL or stray escape in the statusline output.
852
+ function safeTerminalLink(label, url) {
853
+ if (!terminalSupportsHyperlinks()) return label;
854
+ if (typeof url !== 'string' || url.length === 0) return label;
855
+ let parsed;
856
+ try { parsed = new URL(url); } catch { return label; }
857
+ if (parsed.protocol !== 'https:') return label;
858
+ if (!PROMO_LINK_HOSTS.has(parsed.hostname)) return label;
859
+ const cleanLabel = String(label).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '');
860
+ if (cleanLabel.length === 0) return label;
861
+ const ESC = '\u001b';
862
+ return ESC + ']8;;' + parsed.href + ESC + '\\' + cleanLabel + ESC + ']8;;' + ESC + '\\';
863
+ }
864
+
865
+ function getPromoRow(d) {
866
+ try {
867
+ if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
868
+ if (/^(0|false|off|no)$/i.test(String(process.env.RUFLO_FUNNEL || ''))) return null;
869
+ const promo = d && d.promo;
870
+ if (!promo || typeof promo.text !== 'string') return null;
871
+ // Strip control chars / ANSI / bidi overrides and hard-cap length β€”
872
+ // promo copy is data and must never emit its own terminal sequences.
873
+ const text = promo.text
874
+ .replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '')
875
+ .slice(0, 100)
876
+ .trim();
877
+ if (text.length === 0) return null;
878
+ // Split the label from the trailing "Β· manage: ruflo settings" instruction
879
+ // so each part gets styling that matches what it actually IS:
880
+ // 1. label β€” OSC 8 hyperlink + underline. A real clickable link.
881
+ // 2. "manage:" β€” dim. Just a connector word, no action implied.
882
+ // 3. "ruflo settings" β€” bold/bright, NOT underlined. This is a shell
883
+ // command the user TYPES, not a link they CLICK β€” a terminal can
884
+ // never safely execute a command from a click (that would let any
885
+ // server-served message run arbitrary commands), so we deliberately
886
+ // avoid the underline/OSC8 cues that imply "clickable". Bold+bright
887
+ // instead signals "this is the important bit β€” copy/type it".
888
+ // Educational tips have no manage tail and no URL β€” plain text through.
889
+ const manageIdx = text.indexOf(' Β· manage: ');
890
+ const label = manageIdx > 0 ? text.slice(0, manageIdx) : text;
891
+ const manageWord = manageIdx > 0 ? ' Β· manage: ' : '';
892
+ const command = manageIdx > 0 ? text.slice(manageIdx + manageWord.length) : '';
893
+ const UL_ON = '\u001b[4m';
894
+ const UL_OFF = '\u001b[24m';
895
+ const DIM_ON = '\u001b[2m';
896
+ const DIM_OFF = '\u001b[22m';
897
+ const BOLD_ON = '\u001b[1m';
898
+ const BOLD_OFF = '\u001b[22m';
899
+ const linked = promo.url ? UL_ON + safeTerminalLink(label, promo.url) + UL_OFF : label;
900
+ if (!command) return linked;
901
+ return linked + DIM_ON + manageWord + DIM_OFF + BOLD_ON + command + BOLD_OFF;
902
+ } catch (e) {
903
+ return null; // the promo row must never break the statusline
904
+ }
905
+ }
906
+
907
+ // JSON output β€” delegates to CLI for accuracy; caller can use --json flag
908
+ function generateJSON() {
909
+ const d = getStatuslineData();
910
+ const git = getGitInfo();
911
+ return Object.assign({}, d, {
912
+ user: Object.assign({ name: git.name, gitBranch: git.gitBranch }, d.user || {}),
913
+ git: { modified: git.modified, untracked: git.untracked, staged: git.staged, ahead: git.ahead, behind: git.behind },
914
+ lastUpdated: new Date().toISOString(),
915
+ });
916
+ }
917
+
918
+ // ─── Main ───────────────────────────────────────────────────────
919
+ if (process.argv.includes('--json')) {
920
+ console.log(JSON.stringify(generateJSON(), null, 2));
921
+ } else if (process.argv.includes('--compact')) {
922
+ console.log(JSON.stringify(generateJSON()));
923
+ } else {
924
+ console.log(generateStatusline());
925
+ }