mstro-app 0.4.3 → 0.4.4

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 (306) hide show
  1. package/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
  2. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
  3. package/dist/server/cli/headless/claude-invoker-process.js +140 -0
  4. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
  5. package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
  6. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
  7. package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
  8. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
  9. package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
  10. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
  11. package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
  12. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
  13. package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
  14. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
  15. package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
  16. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
  17. package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
  18. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  19. package/dist/server/cli/headless/claude-invoker.js +10 -807
  20. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  21. package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
  22. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
  23. package/dist/server/cli/headless/haiku-assessments.js +281 -0
  24. package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
  25. package/dist/server/cli/headless/headless-logger.d.ts +3 -2
  26. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  27. package/dist/server/cli/headless/headless-logger.js +28 -5
  28. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  29. package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
  30. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
  31. package/dist/server/cli/headless/native-timeout-detector.js +99 -0
  32. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
  33. package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
  34. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  35. package/dist/server/cli/headless/stall-assessor.js +65 -457
  36. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  37. package/dist/server/cli/improvisation-attachments.d.ts +21 -0
  38. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
  39. package/dist/server/cli/improvisation-attachments.js +116 -0
  40. package/dist/server/cli/improvisation-attachments.js.map +1 -0
  41. package/dist/server/cli/improvisation-retry.d.ts +52 -0
  42. package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
  43. package/dist/server/cli/improvisation-retry.js +434 -0
  44. package/dist/server/cli/improvisation-retry.js.map +1 -0
  45. package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
  46. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  47. package/dist/server/cli/improvisation-session-manager.js +117 -1079
  48. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  49. package/dist/server/cli/improvisation-types.d.ts +86 -0
  50. package/dist/server/cli/improvisation-types.d.ts.map +1 -0
  51. package/dist/server/cli/improvisation-types.js +10 -0
  52. package/dist/server/cli/improvisation-types.js.map +1 -0
  53. package/dist/server/cli/prompt-builders.d.ts +68 -0
  54. package/dist/server/cli/prompt-builders.d.ts.map +1 -0
  55. package/dist/server/cli/prompt-builders.js +312 -0
  56. package/dist/server/cli/prompt-builders.js.map +1 -0
  57. package/dist/server/index.js +33 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
  60. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
  61. package/dist/server/mcp/bouncer-haiku.js +152 -0
  62. package/dist/server/mcp/bouncer-haiku.js.map +1 -0
  63. package/dist/server/mcp/bouncer-integration.d.ts +3 -4
  64. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  65. package/dist/server/mcp/bouncer-integration.js +50 -196
  66. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  67. package/dist/server/mcp/security-analysis.d.ts +38 -0
  68. package/dist/server/mcp/security-analysis.d.ts.map +1 -0
  69. package/dist/server/mcp/security-analysis.js +183 -0
  70. package/dist/server/mcp/security-analysis.js.map +1 -0
  71. package/dist/server/mcp/security-audit.d.ts +1 -1
  72. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  73. package/dist/server/mcp/security-patterns.d.ts +1 -25
  74. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  75. package/dist/server/mcp/security-patterns.js +55 -260
  76. package/dist/server/mcp/security-patterns.js.map +1 -1
  77. package/dist/server/server-setup.d.ts +22 -0
  78. package/dist/server/server-setup.d.ts.map +1 -0
  79. package/dist/server/server-setup.js +101 -0
  80. package/dist/server/server-setup.js.map +1 -0
  81. package/dist/server/services/file-explorer-ops.d.ts +24 -0
  82. package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
  83. package/dist/server/services/file-explorer-ops.js +211 -0
  84. package/dist/server/services/file-explorer-ops.js.map +1 -0
  85. package/dist/server/services/files.d.ts +2 -85
  86. package/dist/server/services/files.d.ts.map +1 -1
  87. package/dist/server/services/files.js +7 -427
  88. package/dist/server/services/files.js.map +1 -1
  89. package/dist/server/services/plan/composer.d.ts.map +1 -1
  90. package/dist/server/services/plan/composer.js +2 -1
  91. package/dist/server/services/plan/composer.js.map +1 -1
  92. package/dist/server/services/plan/executor.d.ts.map +1 -1
  93. package/dist/server/services/plan/executor.js +3 -1
  94. package/dist/server/services/plan/executor.js.map +1 -1
  95. package/dist/server/services/plan/parser-core.d.ts +20 -0
  96. package/dist/server/services/plan/parser-core.d.ts.map +1 -0
  97. package/dist/server/services/plan/parser-core.js +350 -0
  98. package/dist/server/services/plan/parser-core.js.map +1 -0
  99. package/dist/server/services/plan/parser-migration.d.ts +5 -0
  100. package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
  101. package/dist/server/services/plan/parser-migration.js +124 -0
  102. package/dist/server/services/plan/parser-migration.js.map +1 -0
  103. package/dist/server/services/plan/parser.d.ts +0 -8
  104. package/dist/server/services/plan/parser.d.ts.map +1 -1
  105. package/dist/server/services/plan/parser.js +50 -569
  106. package/dist/server/services/plan/parser.js.map +1 -1
  107. package/dist/server/services/plan/review-gate.d.ts +2 -0
  108. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  109. package/dist/server/services/plan/review-gate.js +2 -2
  110. package/dist/server/services/plan/review-gate.js.map +1 -1
  111. package/dist/server/services/plan/types.d.ts +2 -0
  112. package/dist/server/services/plan/types.d.ts.map +1 -1
  113. package/dist/server/services/platform-credentials.d.ts +24 -0
  114. package/dist/server/services/platform-credentials.d.ts.map +1 -0
  115. package/dist/server/services/platform-credentials.js +68 -0
  116. package/dist/server/services/platform-credentials.js.map +1 -0
  117. package/dist/server/services/platform.d.ts +1 -31
  118. package/dist/server/services/platform.d.ts.map +1 -1
  119. package/dist/server/services/platform.js +10 -119
  120. package/dist/server/services/platform.js.map +1 -1
  121. package/dist/server/services/terminal/pty-manager.d.ts +7 -97
  122. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  123. package/dist/server/services/terminal/pty-manager.js +53 -266
  124. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  125. package/dist/server/services/terminal/pty-utils.d.ts +57 -0
  126. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
  127. package/dist/server/services/terminal/pty-utils.js +141 -0
  128. package/dist/server/services/terminal/pty-utils.js.map +1 -0
  129. package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
  130. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
  131. package/dist/server/services/websocket/file-definition-handlers.js +153 -0
  132. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
  133. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  134. package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
  135. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  136. package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
  137. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-search-handlers.js +238 -0
  139. package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
  140. package/dist/server/services/websocket/file-utils.js +3 -3
  141. package/dist/server/services/websocket/file-utils.js.map +1 -1
  142. package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
  143. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
  144. package/dist/server/services/websocket/git-branch-handlers.js +110 -0
  145. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
  146. package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
  147. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
  148. package/dist/server/services/websocket/git-diff-handlers.js +123 -0
  149. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
  150. package/dist/server/services/websocket/git-handlers.d.ts +2 -31
  151. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  152. package/dist/server/services/websocket/git-handlers.js +35 -541
  153. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  154. package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
  155. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
  156. package/dist/server/services/websocket/git-log-handlers.js +128 -0
  157. package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
  158. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  159. package/dist/server/services/websocket/git-pr-handlers.js +13 -53
  160. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  161. package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
  162. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
  163. package/dist/server/services/websocket/git-tag-handlers.js +76 -0
  164. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
  165. package/dist/server/services/websocket/git-utils.d.ts +43 -0
  166. package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
  167. package/dist/server/services/websocket/git-utils.js +201 -0
  168. package/dist/server/services/websocket/git-utils.js.map +1 -0
  169. package/dist/server/services/websocket/handler.d.ts +2 -0
  170. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  171. package/dist/server/services/websocket/handler.js +37 -126
  172. package/dist/server/services/websocket/handler.js.map +1 -1
  173. package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
  174. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
  175. package/dist/server/services/websocket/plan-board-handlers.js +218 -0
  176. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
  177. package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
  178. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
  179. package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
  180. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
  181. package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
  182. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  183. package/dist/server/services/websocket/plan-handlers.js +6 -925
  184. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  185. package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
  186. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
  187. package/dist/server/services/websocket/plan-helpers.js +199 -0
  188. package/dist/server/services/websocket/plan-helpers.js.map +1 -0
  189. package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
  190. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
  191. package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
  192. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
  193. package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
  194. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
  195. package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
  196. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
  197. package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
  198. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
  199. package/dist/server/services/websocket/quality-complexity.js +262 -0
  200. package/dist/server/services/websocket/quality-complexity.js.map +1 -0
  201. package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
  202. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
  203. package/dist/server/services/websocket/quality-fix-agent.js +140 -0
  204. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
  205. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  206. package/dist/server/services/websocket/quality-handlers.js +34 -346
  207. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  208. package/dist/server/services/websocket/quality-linting.d.ts +9 -0
  209. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
  210. package/dist/server/services/websocket/quality-linting.js +178 -0
  211. package/dist/server/services/websocket/quality-linting.js.map +1 -0
  212. package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
  213. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
  214. package/dist/server/services/websocket/quality-review-agent.js +206 -0
  215. package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
  216. package/dist/server/services/websocket/quality-service.d.ts +3 -51
  217. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  218. package/dist/server/services/websocket/quality-service.js +9 -651
  219. package/dist/server/services/websocket/quality-service.js.map +1 -1
  220. package/dist/server/services/websocket/quality-tools.d.ts +23 -0
  221. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
  222. package/dist/server/services/websocket/quality-tools.js +208 -0
  223. package/dist/server/services/websocket/quality-tools.js.map +1 -0
  224. package/dist/server/services/websocket/quality-types.d.ts +59 -0
  225. package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
  226. package/dist/server/services/websocket/quality-types.js +101 -0
  227. package/dist/server/services/websocket/quality-types.js.map +1 -0
  228. package/dist/server/services/websocket/session-handlers.d.ts +3 -4
  229. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  230. package/dist/server/services/websocket/session-handlers.js +3 -378
  231. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  232. package/dist/server/services/websocket/session-history.d.ts +4 -0
  233. package/dist/server/services/websocket/session-history.d.ts.map +1 -0
  234. package/dist/server/services/websocket/session-history.js +208 -0
  235. package/dist/server/services/websocket/session-history.js.map +1 -0
  236. package/dist/server/services/websocket/session-initialization.d.ts +5 -0
  237. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
  238. package/dist/server/services/websocket/session-initialization.js +163 -0
  239. package/dist/server/services/websocket/session-initialization.js.map +1 -0
  240. package/dist/server/services/websocket/types.d.ts +12 -2
  241. package/dist/server/services/websocket/types.d.ts.map +1 -1
  242. package/package.json +1 -1
  243. package/server/cli/headless/claude-invoker-process.ts +204 -0
  244. package/server/cli/headless/claude-invoker-stall.ts +164 -0
  245. package/server/cli/headless/claude-invoker-stream.ts +353 -0
  246. package/server/cli/headless/claude-invoker-tools.ts +187 -0
  247. package/server/cli/headless/claude-invoker.ts +15 -1096
  248. package/server/cli/headless/haiku-assessments.ts +365 -0
  249. package/server/cli/headless/headless-logger.ts +26 -5
  250. package/server/cli/headless/native-timeout-detector.ts +117 -0
  251. package/server/cli/headless/stall-assessor.ts +65 -618
  252. package/server/cli/improvisation-attachments.ts +148 -0
  253. package/server/cli/improvisation-retry.ts +602 -0
  254. package/server/cli/improvisation-session-manager.ts +140 -1349
  255. package/server/cli/improvisation-types.ts +98 -0
  256. package/server/cli/prompt-builders.ts +370 -0
  257. package/server/index.ts +35 -246
  258. package/server/mcp/bouncer-haiku.ts +182 -0
  259. package/server/mcp/bouncer-integration.ts +87 -248
  260. package/server/mcp/security-analysis.ts +217 -0
  261. package/server/mcp/security-audit.ts +1 -1
  262. package/server/mcp/security-patterns.ts +60 -283
  263. package/server/server-setup.ts +114 -0
  264. package/server/services/file-explorer-ops.ts +293 -0
  265. package/server/services/files.ts +20 -532
  266. package/server/services/plan/composer.ts +2 -1
  267. package/server/services/plan/executor.ts +3 -1
  268. package/server/services/plan/parser-core.ts +406 -0
  269. package/server/services/plan/parser-migration.ts +128 -0
  270. package/server/services/plan/parser.ts +52 -620
  271. package/server/services/plan/review-gate.ts +4 -2
  272. package/server/services/plan/types.ts +2 -0
  273. package/server/services/platform-credentials.ts +83 -0
  274. package/server/services/platform.ts +15 -141
  275. package/server/services/terminal/pty-manager.ts +66 -313
  276. package/server/services/terminal/pty-utils.ts +176 -0
  277. package/server/services/websocket/file-definition-handlers.ts +165 -0
  278. package/server/services/websocket/file-explorer-handlers.ts +37 -452
  279. package/server/services/websocket/file-search-handlers.ts +291 -0
  280. package/server/services/websocket/file-utils.ts +3 -3
  281. package/server/services/websocket/git-branch-handlers.ts +130 -0
  282. package/server/services/websocket/git-diff-handlers.ts +140 -0
  283. package/server/services/websocket/git-handlers.ts +40 -625
  284. package/server/services/websocket/git-log-handlers.ts +149 -0
  285. package/server/services/websocket/git-pr-handlers.ts +17 -62
  286. package/server/services/websocket/git-tag-handlers.ts +91 -0
  287. package/server/services/websocket/git-utils.ts +230 -0
  288. package/server/services/websocket/handler.ts +39 -126
  289. package/server/services/websocket/plan-board-handlers.ts +277 -0
  290. package/server/services/websocket/plan-execution-handlers.ts +184 -0
  291. package/server/services/websocket/plan-handlers.ts +8 -1114
  292. package/server/services/websocket/plan-helpers.ts +215 -0
  293. package/server/services/websocket/plan-issue-handlers.ts +204 -0
  294. package/server/services/websocket/plan-sprint-handlers.ts +252 -0
  295. package/server/services/websocket/quality-complexity.ts +294 -0
  296. package/server/services/websocket/quality-fix-agent.ts +181 -0
  297. package/server/services/websocket/quality-handlers.ts +36 -404
  298. package/server/services/websocket/quality-linting.ts +187 -0
  299. package/server/services/websocket/quality-review-agent.ts +246 -0
  300. package/server/services/websocket/quality-service.ts +11 -762
  301. package/server/services/websocket/quality-tools.ts +209 -0
  302. package/server/services/websocket/quality-types.ts +169 -0
  303. package/server/services/websocket/session-handlers.ts +5 -437
  304. package/server/services/websocket/session-history.ts +222 -0
  305. package/server/services/websocket/session-initialization.ts +209 -0
  306. package/server/services/websocket/types.ts +17 -0
@@ -0,0 +1,294 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ import { existsSync } from 'node:fs';
5
+ import { extname, join, relative } from 'node:path';
6
+ import { runCommand, type SourceFile } from './quality-tools.js';
7
+ import { biomeDiagToFinding, type Ecosystem, FUNCTION_LENGTH_THRESHOLD, isBiomeComplexityDiagnostic, isEslintComplexityRule, type QualityFinding } from './quality-types.js';
8
+
9
+ // ============================================================================
10
+ // Function Length Analysis
11
+ // ============================================================================
12
+
13
+ interface FunctionInfo {
14
+ name: string;
15
+ file: string;
16
+ startLine: number;
17
+ lines: number;
18
+ }
19
+
20
+ const JS_FUNC_PATTERN = /^(\s*)(export\s+)?(async\s+)?function\s+(\w+)|^(\s*)(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(|^(\s*)(public|private|protected)?\s*(async\s+)?(\w+)\s*\(/;
21
+
22
+ function countBraceDeltas(line: string): number {
23
+ let delta = 0;
24
+ for (const ch of line) {
25
+ if (ch === '{') delta++;
26
+ else if (ch === '}') delta--;
27
+ }
28
+ return delta;
29
+ }
30
+
31
+ function matchJsFuncStart(line: string): { name: string; indent: number } | null {
32
+ const match = JS_FUNC_PATTERN.exec(line);
33
+ if (!match) return null;
34
+ const name = match[4] || match[8] || match[13] || 'anonymous';
35
+ const indent = (match[1] || match[5] || match[10] || '').length;
36
+ return { name, indent };
37
+ }
38
+
39
+ function extractJsFunctions(file: SourceFile): FunctionInfo[] {
40
+ const functions: FunctionInfo[] = [];
41
+ const lines = file.content.split('\n');
42
+ let braceDepth = 0;
43
+ let currentFunc: { name: string; startLine: number; indent: number } | null = null;
44
+ let funcStartBraceDepth = 0;
45
+
46
+ for (let i = 0; i < lines.length; i++) {
47
+ if (!currentFunc) {
48
+ const funcStart = matchJsFuncStart(lines[i]);
49
+ if (funcStart) {
50
+ currentFunc = { name: funcStart.name, startLine: i + 1, indent: funcStart.indent };
51
+ funcStartBraceDepth = braceDepth;
52
+ }
53
+ }
54
+
55
+ braceDepth += countBraceDeltas(lines[i]);
56
+
57
+ if (currentFunc && braceDepth <= funcStartBraceDepth && i > currentFunc.startLine - 1) {
58
+ functions.push({
59
+ name: currentFunc.name,
60
+ file: file.relativePath,
61
+ startLine: currentFunc.startLine,
62
+ lines: i + 1 - currentFunc.startLine + 1,
63
+ });
64
+ currentFunc = null;
65
+ }
66
+ }
67
+
68
+ return functions;
69
+ }
70
+
71
+ function extractPyFunctions(file: SourceFile): FunctionInfo[] {
72
+ const functions: FunctionInfo[] = [];
73
+ const lines = file.content.split('\n');
74
+ const defPattern = /^(\s*)(async\s+)?def\s+(\w+)/;
75
+ let currentFunc: { name: string; startLine: number; indent: number } | null = null;
76
+
77
+ for (let i = 0; i < lines.length; i++) {
78
+ const match = defPattern.exec(lines[i]);
79
+ if (match) {
80
+ if (currentFunc) {
81
+ functions.push({
82
+ name: currentFunc.name,
83
+ file: file.relativePath,
84
+ startLine: currentFunc.startLine,
85
+ lines: i - currentFunc.startLine + 1,
86
+ });
87
+ }
88
+ currentFunc = { name: match[3], startLine: i + 1, indent: match[1].length };
89
+ } else if (currentFunc && lines[i].trim() && !lines[i].startsWith(' '.repeat(currentFunc.indent + 1)) && !lines[i].startsWith('\t')) {
90
+ functions.push({
91
+ name: currentFunc.name,
92
+ file: file.relativePath,
93
+ startLine: currentFunc.startLine,
94
+ lines: i - currentFunc.startLine + 1,
95
+ });
96
+ currentFunc = null;
97
+ }
98
+ }
99
+ if (currentFunc) {
100
+ functions.push({
101
+ name: currentFunc.name,
102
+ file: file.relativePath,
103
+ startLine: currentFunc.startLine,
104
+ lines: lines.length - currentFunc.startLine + 1,
105
+ });
106
+ }
107
+
108
+ return functions;
109
+ }
110
+
111
+ function extractFunctions(file: SourceFile): FunctionInfo[] {
112
+ const ext = extname(file.path).toLowerCase();
113
+ if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) return extractJsFunctions(file);
114
+ if (['.py', '.pyi'].includes(ext)) return extractPyFunctions(file);
115
+ return [];
116
+ }
117
+
118
+ export function analyzeFunctionLength(files: SourceFile[]): { score: number; findings: QualityFinding[]; issueCount: number } {
119
+ const allFunctions: FunctionInfo[] = [];
120
+ for (const file of files) {
121
+ allFunctions.push(...extractFunctions(file));
122
+ }
123
+
124
+ if (allFunctions.length === 0) return { score: 100, findings: [], issueCount: 0 };
125
+
126
+ const findings: QualityFinding[] = [];
127
+ let totalScore = 0;
128
+
129
+ for (const func of allFunctions) {
130
+ const ratio = Math.max(1, func.lines / FUNCTION_LENGTH_THRESHOLD);
131
+ const funcScore = 100 / ratio ** 1.5;
132
+ totalScore += funcScore;
133
+
134
+ if (func.lines > FUNCTION_LENGTH_THRESHOLD) {
135
+ findings.push({
136
+ severity: func.lines > FUNCTION_LENGTH_THRESHOLD * 3 ? 'high' : func.lines > FUNCTION_LENGTH_THRESHOLD * 2 ? 'medium' : 'low',
137
+ category: 'function-length',
138
+ file: func.file,
139
+ line: func.startLine,
140
+ title: `${func.name}() has ${func.lines} lines (threshold: ${FUNCTION_LENGTH_THRESHOLD})`,
141
+ description: `Function "${func.name}" exceeds the recommended length by ${func.lines - FUNCTION_LENGTH_THRESHOLD} lines.`,
142
+ });
143
+ }
144
+ }
145
+
146
+ const score = Math.round(totalScore / allFunctions.length);
147
+ return { score: Math.min(100, score), findings: findings.slice(0, 50), issueCount: findings.length };
148
+ }
149
+
150
+ // ============================================================================
151
+ // Complexity Analysis (Biome, ESLint, radon)
152
+ // ============================================================================
153
+
154
+ function computeComplexityScore(findings: QualityFinding[]): number {
155
+ let penalty = 0;
156
+ for (const f of findings) {
157
+ if (f.severity === 'high' || f.severity === 'critical') penalty += 8;
158
+ else if (f.severity === 'medium') penalty += 5;
159
+ else penalty += 3;
160
+ }
161
+ return Math.max(0, 100 - penalty);
162
+ }
163
+
164
+ async function complexityFromBiome(dirPath: string): Promise<QualityFinding[] | null> {
165
+ const hasBiomeConfig = existsSync(join(dirPath, 'biome.json')) || existsSync(join(dirPath, 'biome.jsonc'));
166
+ if (!hasBiomeConfig) return null;
167
+
168
+ const result = await runCommand('npx', ['@biomejs/biome', 'lint', '--reporter=json', '.'], dirPath);
169
+ if (result.exitCode > 1) return null;
170
+
171
+ try {
172
+ const parsed = JSON.parse(result.stdout);
173
+ if (!parsed.diagnostics) return [];
174
+ return parsed.diagnostics
175
+ .filter(isBiomeComplexityDiagnostic)
176
+ .map((d: Record<string, unknown>) => biomeDiagToFinding(d, 'complexity'));
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ async function complexityFromEslint(dirPath: string): Promise<QualityFinding[] | null> {
183
+ const result = await runCommand('npx', ['eslint', '--format=json', '.'], dirPath);
184
+ if (result.exitCode > 1 && !result.stdout.trim().startsWith('[')) return null;
185
+
186
+ const findings: QualityFinding[] = [];
187
+ try {
188
+ const parsed = JSON.parse(result.stdout);
189
+ for (const file of parsed) {
190
+ for (const msg of file.messages || []) {
191
+ if (!isEslintComplexityRule(msg.ruleId)) continue;
192
+ findings.push({
193
+ severity: msg.severity === 2 ? 'high' : 'medium',
194
+ category: 'complexity',
195
+ file: relative(dirPath, file.filePath),
196
+ line: msg.line ?? null,
197
+ title: msg.ruleId || 'complexity',
198
+ description: msg.message,
199
+ });
200
+ }
201
+ }
202
+ } catch {
203
+ return null;
204
+ }
205
+
206
+ return findings;
207
+ }
208
+
209
+ function radonFuncToFinding(filePath: string, func: Record<string, unknown>): QualityFinding | null {
210
+ const cc = func.complexity as number;
211
+ if (cc <= 10) return null;
212
+ return {
213
+ severity: cc > 20 ? 'high' : cc > 15 ? 'medium' : 'low',
214
+ category: 'complexity',
215
+ file: filePath,
216
+ line: (func.lineno as number) ?? null,
217
+ title: `${func.name}() has cyclomatic complexity ${cc}`,
218
+ description: `Complexity of ${cc} exceeds threshold of 10. Rank: ${func.rank}. Consider refactoring.`,
219
+ };
220
+ }
221
+
222
+ async function complexityFromRadon(dirPath: string): Promise<QualityFinding[] | null> {
223
+ const result = await runCommand('radon', ['cc', '--json', '.'], dirPath);
224
+ if (result.exitCode !== 0 && !result.stdout.trim().startsWith('{')) return null;
225
+
226
+ try {
227
+ const parsed = JSON.parse(result.stdout) as Record<string, Array<Record<string, unknown>>>;
228
+ const findings: QualityFinding[] = [];
229
+ for (const [filePath, functions] of Object.entries(parsed)) {
230
+ for (const func of functions) {
231
+ const finding = radonFuncToFinding(filePath, func);
232
+ if (finding) findings.push(finding);
233
+ }
234
+ }
235
+ return findings;
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ async function analyzeNodeComplexity(
242
+ dirPath: string,
243
+ installed: Set<string> | null,
244
+ ): Promise<QualityFinding[] | null> {
245
+ const hasCapableTool = !installed || installed.has('biome') || installed.has('eslint');
246
+ if (!hasCapableTool) return null;
247
+
248
+ const hasBiomeConfig = existsSync(join(dirPath, 'biome.json')) || existsSync(join(dirPath, 'biome.jsonc'));
249
+ if (hasBiomeConfig) {
250
+ const findings = await complexityFromBiome(dirPath);
251
+ if (findings) return findings;
252
+ }
253
+ return complexityFromEslint(dirPath);
254
+ }
255
+
256
+ async function analyzePythonComplexity(
257
+ dirPath: string,
258
+ installed: Set<string> | null,
259
+ ): Promise<QualityFinding[] | null> {
260
+ const hasRadon = !installed || installed.has('radon');
261
+ if (!hasRadon) return null;
262
+ return complexityFromRadon(dirPath);
263
+ }
264
+
265
+ export async function analyzeComplexity(
266
+ dirPath: string,
267
+ ecosystems: Ecosystem[],
268
+ installedToolNames?: string[],
269
+ ): Promise<{ score: number; findings: QualityFinding[]; issueCount: number; available: boolean }> {
270
+ const allFindings: QualityFinding[] = [];
271
+ const installed = installedToolNames ? new Set(installedToolNames) : null;
272
+ let canAnalyze = false;
273
+
274
+ for (const ecosystem of ecosystems) {
275
+ const analyze = ecosystem === 'node' ? analyzeNodeComplexity : ecosystem === 'python' ? analyzePythonComplexity : null;
276
+ if (!analyze) continue;
277
+ const findings = await analyze(dirPath, installed);
278
+ if (findings) {
279
+ canAnalyze = true;
280
+ allFindings.push(...findings);
281
+ }
282
+ }
283
+
284
+ if (!canAnalyze) {
285
+ return { score: 0, findings: [], issueCount: 0, available: false };
286
+ }
287
+
288
+ return {
289
+ score: allFindings.length > 0 ? computeComplexityScore(allFindings) : 100,
290
+ findings: allFindings.slice(0, 50),
291
+ issueCount: allFindings.length,
292
+ available: true,
293
+ };
294
+ }
@@ -0,0 +1,181 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Quality Fix Agent — AI-powered issue fixing using Claude Code headless runner.
6
+ *
7
+ * Builds the fix prompt, runs the agent, re-scans, and persists updated results.
8
+ */
9
+
10
+ import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
11
+ import { HeadlessRunner } from '../../cli/headless/index.js';
12
+ import type { ToolUseEvent } from '../../cli/headless/types.js';
13
+ import type { HandlerContext } from './handler-context.js';
14
+ import type { QualityPersistence } from './quality-persistence.js';
15
+ import { detectTools, runQualityScan } from './quality-service.js';
16
+ import type { WSContext } from './types.js';
17
+
18
+ // ── Types ─────────────────────────────────────────────────────
19
+
20
+ export interface FindingForFix {
21
+ severity: string;
22
+ category: string;
23
+ file: string;
24
+ line: number | null;
25
+ title: string;
26
+ description: string;
27
+ suggestion?: string;
28
+ }
29
+
30
+ // ── Progress callback ─────────────────────────────────────────
31
+
32
+ const TOOL_MESSAGES: Record<string, string> = {
33
+ Read: 'Reading files to understand issues...',
34
+ Edit: 'Applying fixes...',
35
+ Write: 'Writing fixes...',
36
+ Grep: 'Searching for related code...',
37
+ Bash: 'Running verification...',
38
+ };
39
+
40
+ export function createToolProgressCallback(ctx: HandlerContext, ws: WSContext, reportPath: string) {
41
+ const seenTools = new Set<string>();
42
+ return (event: ToolUseEvent) => {
43
+ if (event.type === 'tool_start' && event.toolName && !seenTools.has(event.toolName)) {
44
+ seenTools.add(event.toolName);
45
+ const message = TOOL_MESSAGES[event.toolName];
46
+ if (message) {
47
+ ctx.send(ws, { type: 'qualityFixProgress', data: { path: reportPath, message } });
48
+ }
49
+ }
50
+ if (event.type === 'tool_complete' && event.toolName === 'Edit' && event.completeInput?.file_path) {
51
+ ctx.send(ws, {
52
+ type: 'qualityFixProgress',
53
+ data: { path: reportPath, message: `Fixed ${String(event.completeInput.file_path).split('/').slice(-2).join('/')}` },
54
+ });
55
+ }
56
+ };
57
+ }
58
+
59
+ // ── Prompt ────────────────────────────────────────────────────
60
+
61
+ function buildFixPrompt(findings: FindingForFix[], section?: string): string {
62
+ const filtered = section ? findings.filter((f) => f.category === section) : findings;
63
+ const sorted = filtered.sort((a, b) => {
64
+ const order: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
65
+ return (order[a.severity] ?? 4) - (order[b.severity] ?? 4);
66
+ });
67
+
68
+ const issueList = sorted.slice(0, 30).map((f, i) => {
69
+ const loc = f.line ? `${f.file}:${f.line}` : f.file;
70
+ const parts = [`${i + 1}. [${f.severity.toUpperCase()}] ${loc} — ${f.title}`];
71
+ if (f.description) parts.push(` ${f.description}`);
72
+ if (f.suggestion) parts.push(` Suggestion: ${f.suggestion}`);
73
+ return parts.join('\n');
74
+ }).join('\n\n');
75
+
76
+ return `You are a code quality fix agent. Fix the following quality issues in the codebase.
77
+
78
+ ## Issues to Fix (${sorted.length} total, showing top ${Math.min(30, sorted.length)})
79
+
80
+ ${issueList}
81
+
82
+ ## Rules
83
+
84
+ - Fix each issue by editing the relevant file at the specified location.
85
+ - For complexity issues: refactor into smaller functions. For long files: split or extract modules. For long functions: break into smaller functions.
86
+ - For security issues: apply the suggested fix or use secure coding best practices.
87
+ - For bugs: fix the root cause, not just the symptom.
88
+ - For linting/formatting: apply the standard for the project.
89
+ - Do NOT introduce new issues. Make minimal, focused changes.
90
+ - After fixing, verify the changes compile/pass linting if tools are available.
91
+ - Work through the issues systematically from most to least severe.`;
92
+ }
93
+
94
+ // ── Handler ───────────────────────────────────────────────────
95
+
96
+ const activeFixes = new Set<string>();
97
+
98
+ export async function handleFixIssues(
99
+ ctx: HandlerContext,
100
+ ws: WSContext,
101
+ reportPath: string,
102
+ dirPath: string,
103
+ workingDir: string,
104
+ section: string | undefined,
105
+ findings: FindingForFix[],
106
+ getPersistence: (dir: string) => QualityPersistence,
107
+ ): Promise<void> {
108
+ if (activeFixes.has(dirPath)) {
109
+ ctx.send(ws, {
110
+ type: 'qualityError',
111
+ data: { path: reportPath, error: 'A fix operation is already running for this directory.' },
112
+ });
113
+ return;
114
+ }
115
+
116
+ if (findings.length === 0) {
117
+ ctx.send(ws, {
118
+ type: 'qualityError',
119
+ data: { path: reportPath, error: 'No findings to fix.' },
120
+ });
121
+ return;
122
+ }
123
+
124
+ activeFixes.add(dirPath);
125
+ try {
126
+ ctx.send(ws, {
127
+ type: 'qualityFixProgress',
128
+ data: { path: reportPath, message: 'Starting Claude Code to fix issues...' },
129
+ });
130
+
131
+ const prompt = buildFixPrompt(findings, section);
132
+
133
+ const runner = new HeadlessRunner({
134
+ workingDir: dirPath,
135
+ directPrompt: prompt,
136
+ stallWarningMs: 120_000,
137
+ stallKillMs: 600_000,
138
+ stallHardCapMs: 900_000,
139
+ toolUseCallback: createToolProgressCallback(ctx, ws, reportPath),
140
+ });
141
+
142
+ await runWithFileLogger('code-review-fix', () => runner.run());
143
+
144
+ ctx.send(ws, {
145
+ type: 'qualityFixProgress',
146
+ data: { path: reportPath, message: 'Fixes applied. Re-running quality checks...' },
147
+ });
148
+
149
+ // Re-run quality scan after fixing
150
+ const { tools: detectedTools } = await detectTools(dirPath);
151
+ const installedToolNames = detectedTools.filter((t) => t.installed).map((t) => t.name);
152
+
153
+ const results = await runQualityScan(dirPath, (progress) => {
154
+ ctx.send(ws, {
155
+ type: 'qualityScanProgress',
156
+ data: { path: reportPath, progress },
157
+ });
158
+ }, installedToolNames);
159
+
160
+ ctx.send(ws, {
161
+ type: 'qualityFixComplete',
162
+ data: { path: reportPath, results },
163
+ });
164
+
165
+ // Persist
166
+ try {
167
+ const persistence = getPersistence(workingDir);
168
+ persistence.saveReport(reportPath, results);
169
+ persistence.appendHistory(results, reportPath);
170
+ } catch {
171
+ // Persistence failure should not break the fix flow
172
+ }
173
+ } catch (error) {
174
+ ctx.send(ws, {
175
+ type: 'qualityError',
176
+ data: { path: reportPath, error: error instanceof Error ? error.message : String(error) },
177
+ });
178
+ } finally {
179
+ activeFixes.delete(dirPath);
180
+ }
181
+ }