mstro-app 0.4.2 → 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 (342) hide show
  1. package/bin/mstro.js +119 -40
  2. package/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
  3. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
  4. package/dist/server/cli/headless/claude-invoker-process.js +140 -0
  5. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
  6. package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
  7. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
  8. package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
  9. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
  10. package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
  11. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
  12. package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
  13. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
  14. package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
  15. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
  16. package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
  17. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
  18. package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
  19. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  20. package/dist/server/cli/headless/claude-invoker.js +10 -804
  21. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  22. package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
  23. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
  24. package/dist/server/cli/headless/haiku-assessments.js +281 -0
  25. package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
  26. package/dist/server/cli/headless/headless-logger.d.ts +3 -2
  27. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  28. package/dist/server/cli/headless/headless-logger.js +28 -5
  29. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  30. package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
  31. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
  32. package/dist/server/cli/headless/native-timeout-detector.js +99 -0
  33. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
  34. package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
  35. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  36. package/dist/server/cli/headless/stall-assessor.js +65 -457
  37. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  38. package/dist/server/cli/headless/types.d.ts +4 -1
  39. package/dist/server/cli/headless/types.d.ts.map +1 -1
  40. package/dist/server/cli/improvisation-attachments.d.ts +21 -0
  41. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
  42. package/dist/server/cli/improvisation-attachments.js +116 -0
  43. package/dist/server/cli/improvisation-attachments.js.map +1 -0
  44. package/dist/server/cli/improvisation-retry.d.ts +52 -0
  45. package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
  46. package/dist/server/cli/improvisation-retry.js +434 -0
  47. package/dist/server/cli/improvisation-retry.js.map +1 -0
  48. package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
  49. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  50. package/dist/server/cli/improvisation-session-manager.js +117 -1079
  51. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  52. package/dist/server/cli/improvisation-types.d.ts +86 -0
  53. package/dist/server/cli/improvisation-types.d.ts.map +1 -0
  54. package/dist/server/cli/improvisation-types.js +10 -0
  55. package/dist/server/cli/improvisation-types.js.map +1 -0
  56. package/dist/server/cli/prompt-builders.d.ts +68 -0
  57. package/dist/server/cli/prompt-builders.d.ts.map +1 -0
  58. package/dist/server/cli/prompt-builders.js +312 -0
  59. package/dist/server/cli/prompt-builders.js.map +1 -0
  60. package/dist/server/index.js +33 -212
  61. package/dist/server/index.js.map +1 -1
  62. package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
  63. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
  64. package/dist/server/mcp/bouncer-haiku.js +152 -0
  65. package/dist/server/mcp/bouncer-haiku.js.map +1 -0
  66. package/dist/server/mcp/bouncer-integration.d.ts +3 -4
  67. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  68. package/dist/server/mcp/bouncer-integration.js +50 -196
  69. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  70. package/dist/server/mcp/security-analysis.d.ts +38 -0
  71. package/dist/server/mcp/security-analysis.d.ts.map +1 -0
  72. package/dist/server/mcp/security-analysis.js +183 -0
  73. package/dist/server/mcp/security-analysis.js.map +1 -0
  74. package/dist/server/mcp/security-audit.d.ts +1 -1
  75. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  76. package/dist/server/mcp/security-patterns.d.ts +1 -25
  77. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  78. package/dist/server/mcp/security-patterns.js +55 -260
  79. package/dist/server/mcp/security-patterns.js.map +1 -1
  80. package/dist/server/server-setup.d.ts +22 -0
  81. package/dist/server/server-setup.d.ts.map +1 -0
  82. package/dist/server/server-setup.js +101 -0
  83. package/dist/server/server-setup.js.map +1 -0
  84. package/dist/server/services/file-explorer-ops.d.ts +24 -0
  85. package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
  86. package/dist/server/services/file-explorer-ops.js +211 -0
  87. package/dist/server/services/file-explorer-ops.js.map +1 -0
  88. package/dist/server/services/files.d.ts +2 -85
  89. package/dist/server/services/files.d.ts.map +1 -1
  90. package/dist/server/services/files.js +7 -427
  91. package/dist/server/services/files.js.map +1 -1
  92. package/dist/server/services/plan/composer.d.ts +1 -1
  93. package/dist/server/services/plan/composer.d.ts.map +1 -1
  94. package/dist/server/services/plan/composer.js +118 -32
  95. package/dist/server/services/plan/composer.js.map +1 -1
  96. package/dist/server/services/plan/config-installer.d.ts +25 -0
  97. package/dist/server/services/plan/config-installer.d.ts.map +1 -0
  98. package/dist/server/services/plan/config-installer.js +182 -0
  99. package/dist/server/services/plan/config-installer.js.map +1 -0
  100. package/dist/server/services/plan/dependency-resolver.d.ts +1 -1
  101. package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
  102. package/dist/server/services/plan/dependency-resolver.js +4 -1
  103. package/dist/server/services/plan/dependency-resolver.js.map +1 -1
  104. package/dist/server/services/plan/executor.d.ts +38 -74
  105. package/dist/server/services/plan/executor.d.ts.map +1 -1
  106. package/dist/server/services/plan/executor.js +274 -460
  107. package/dist/server/services/plan/executor.js.map +1 -1
  108. package/dist/server/services/plan/front-matter.d.ts +18 -0
  109. package/dist/server/services/plan/front-matter.d.ts.map +1 -0
  110. package/dist/server/services/plan/front-matter.js +44 -0
  111. package/dist/server/services/plan/front-matter.js.map +1 -0
  112. package/dist/server/services/plan/output-manager.d.ts +22 -0
  113. package/dist/server/services/plan/output-manager.d.ts.map +1 -0
  114. package/dist/server/services/plan/output-manager.js +97 -0
  115. package/dist/server/services/plan/output-manager.js.map +1 -0
  116. package/dist/server/services/plan/parser-core.d.ts +20 -0
  117. package/dist/server/services/plan/parser-core.d.ts.map +1 -0
  118. package/dist/server/services/plan/parser-core.js +350 -0
  119. package/dist/server/services/plan/parser-core.js.map +1 -0
  120. package/dist/server/services/plan/parser-migration.d.ts +5 -0
  121. package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
  122. package/dist/server/services/plan/parser-migration.js +124 -0
  123. package/dist/server/services/plan/parser-migration.js.map +1 -0
  124. package/dist/server/services/plan/parser.d.ts +11 -3
  125. package/dist/server/services/plan/parser.d.ts.map +1 -1
  126. package/dist/server/services/plan/parser.js +184 -369
  127. package/dist/server/services/plan/parser.js.map +1 -1
  128. package/dist/server/services/plan/prompt-builder.d.ts +17 -0
  129. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -0
  130. package/dist/server/services/plan/prompt-builder.js +137 -0
  131. package/dist/server/services/plan/prompt-builder.js.map +1 -0
  132. package/dist/server/services/plan/review-gate.d.ts +28 -0
  133. package/dist/server/services/plan/review-gate.d.ts.map +1 -0
  134. package/dist/server/services/plan/review-gate.js +191 -0
  135. package/dist/server/services/plan/review-gate.js.map +1 -0
  136. package/dist/server/services/plan/state-reconciler.d.ts +1 -1
  137. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
  138. package/dist/server/services/plan/state-reconciler.js +59 -7
  139. package/dist/server/services/plan/state-reconciler.js.map +1 -1
  140. package/dist/server/services/plan/types.d.ts +68 -0
  141. package/dist/server/services/plan/types.d.ts.map +1 -1
  142. package/dist/server/services/platform-credentials.d.ts +24 -0
  143. package/dist/server/services/platform-credentials.d.ts.map +1 -0
  144. package/dist/server/services/platform-credentials.js +68 -0
  145. package/dist/server/services/platform-credentials.js.map +1 -0
  146. package/dist/server/services/platform.d.ts +1 -31
  147. package/dist/server/services/platform.d.ts.map +1 -1
  148. package/dist/server/services/platform.js +11 -109
  149. package/dist/server/services/platform.js.map +1 -1
  150. package/dist/server/services/terminal/pty-manager.d.ts +7 -97
  151. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  152. package/dist/server/services/terminal/pty-manager.js +53 -266
  153. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  154. package/dist/server/services/terminal/pty-utils.d.ts +57 -0
  155. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
  156. package/dist/server/services/terminal/pty-utils.js +141 -0
  157. package/dist/server/services/terminal/pty-utils.js.map +1 -0
  158. package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
  159. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
  160. package/dist/server/services/websocket/file-definition-handlers.js +153 -0
  161. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
  162. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  163. package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
  164. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  165. package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
  166. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
  167. package/dist/server/services/websocket/file-search-handlers.js +238 -0
  168. package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
  169. package/dist/server/services/websocket/file-utils.js +3 -3
  170. package/dist/server/services/websocket/file-utils.js.map +1 -1
  171. package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
  172. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
  173. package/dist/server/services/websocket/git-branch-handlers.js +110 -0
  174. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
  175. package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
  176. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
  177. package/dist/server/services/websocket/git-diff-handlers.js +123 -0
  178. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
  179. package/dist/server/services/websocket/git-handlers.d.ts +2 -31
  180. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  181. package/dist/server/services/websocket/git-handlers.js +35 -541
  182. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  183. package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
  184. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
  185. package/dist/server/services/websocket/git-log-handlers.js +128 -0
  186. package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
  187. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  188. package/dist/server/services/websocket/git-pr-handlers.js +13 -53
  189. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  190. package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
  191. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
  192. package/dist/server/services/websocket/git-tag-handlers.js +76 -0
  193. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
  194. package/dist/server/services/websocket/git-utils.d.ts +43 -0
  195. package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
  196. package/dist/server/services/websocket/git-utils.js +201 -0
  197. package/dist/server/services/websocket/git-utils.js.map +1 -0
  198. package/dist/server/services/websocket/handler.d.ts +2 -0
  199. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  200. package/dist/server/services/websocket/handler.js +37 -112
  201. package/dist/server/services/websocket/handler.js.map +1 -1
  202. package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
  203. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
  204. package/dist/server/services/websocket/plan-board-handlers.js +218 -0
  205. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
  206. package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
  207. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
  208. package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
  209. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
  210. package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
  211. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  212. package/dist/server/services/websocket/plan-handlers.js +21 -462
  213. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  214. package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
  215. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
  216. package/dist/server/services/websocket/plan-helpers.js +199 -0
  217. package/dist/server/services/websocket/plan-helpers.js.map +1 -0
  218. package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
  219. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
  220. package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
  221. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
  222. package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
  223. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
  224. package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
  225. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
  226. package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
  227. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
  228. package/dist/server/services/websocket/quality-complexity.js +262 -0
  229. package/dist/server/services/websocket/quality-complexity.js.map +1 -0
  230. package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
  231. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
  232. package/dist/server/services/websocket/quality-fix-agent.js +140 -0
  233. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
  234. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  235. package/dist/server/services/websocket/quality-handlers.js +34 -346
  236. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  237. package/dist/server/services/websocket/quality-linting.d.ts +9 -0
  238. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
  239. package/dist/server/services/websocket/quality-linting.js +178 -0
  240. package/dist/server/services/websocket/quality-linting.js.map +1 -0
  241. package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
  242. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
  243. package/dist/server/services/websocket/quality-review-agent.js +206 -0
  244. package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
  245. package/dist/server/services/websocket/quality-service.d.ts +3 -51
  246. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  247. package/dist/server/services/websocket/quality-service.js +9 -651
  248. package/dist/server/services/websocket/quality-service.js.map +1 -1
  249. package/dist/server/services/websocket/quality-tools.d.ts +23 -0
  250. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
  251. package/dist/server/services/websocket/quality-tools.js +208 -0
  252. package/dist/server/services/websocket/quality-tools.js.map +1 -0
  253. package/dist/server/services/websocket/quality-types.d.ts +59 -0
  254. package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
  255. package/dist/server/services/websocket/quality-types.js +101 -0
  256. package/dist/server/services/websocket/quality-types.js.map +1 -0
  257. package/dist/server/services/websocket/session-handlers.d.ts +3 -4
  258. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  259. package/dist/server/services/websocket/session-handlers.js +3 -378
  260. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  261. package/dist/server/services/websocket/session-history.d.ts +4 -0
  262. package/dist/server/services/websocket/session-history.d.ts.map +1 -0
  263. package/dist/server/services/websocket/session-history.js +208 -0
  264. package/dist/server/services/websocket/session-history.js.map +1 -0
  265. package/dist/server/services/websocket/session-initialization.d.ts +5 -0
  266. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
  267. package/dist/server/services/websocket/session-initialization.js +163 -0
  268. package/dist/server/services/websocket/session-initialization.js.map +1 -0
  269. package/dist/server/services/websocket/types.d.ts +12 -2
  270. package/dist/server/services/websocket/types.d.ts.map +1 -1
  271. package/package.json +1 -2
  272. package/server/cli/headless/claude-invoker-process.ts +204 -0
  273. package/server/cli/headless/claude-invoker-stall.ts +164 -0
  274. package/server/cli/headless/claude-invoker-stream.ts +353 -0
  275. package/server/cli/headless/claude-invoker-tools.ts +187 -0
  276. package/server/cli/headless/claude-invoker.ts +15 -1092
  277. package/server/cli/headless/haiku-assessments.ts +365 -0
  278. package/server/cli/headless/headless-logger.ts +26 -5
  279. package/server/cli/headless/native-timeout-detector.ts +117 -0
  280. package/server/cli/headless/stall-assessor.ts +65 -618
  281. package/server/cli/headless/types.ts +4 -1
  282. package/server/cli/improvisation-attachments.ts +148 -0
  283. package/server/cli/improvisation-retry.ts +602 -0
  284. package/server/cli/improvisation-session-manager.ts +140 -1349
  285. package/server/cli/improvisation-types.ts +98 -0
  286. package/server/cli/prompt-builders.ts +370 -0
  287. package/server/index.ts +35 -246
  288. package/server/mcp/bouncer-haiku.ts +182 -0
  289. package/server/mcp/bouncer-integration.ts +87 -248
  290. package/server/mcp/security-analysis.ts +217 -0
  291. package/server/mcp/security-audit.ts +1 -1
  292. package/server/mcp/security-patterns.ts +60 -283
  293. package/server/server-setup.ts +114 -0
  294. package/server/services/file-explorer-ops.ts +293 -0
  295. package/server/services/files.ts +20 -532
  296. package/server/services/plan/composer.ts +140 -35
  297. package/server/services/plan/config-installer.ts +187 -0
  298. package/server/services/plan/dependency-resolver.ts +4 -1
  299. package/server/services/plan/executor.ts +281 -488
  300. package/server/services/plan/front-matter.ts +48 -0
  301. package/server/services/plan/output-manager.ts +113 -0
  302. package/server/services/plan/parser-core.ts +406 -0
  303. package/server/services/plan/parser-migration.ts +128 -0
  304. package/server/services/plan/parser.ts +188 -394
  305. package/server/services/plan/prompt-builder.ts +161 -0
  306. package/server/services/plan/review-gate.ts +212 -0
  307. package/server/services/plan/state-reconciler.ts +68 -7
  308. package/server/services/plan/types.ts +101 -1
  309. package/server/services/platform-credentials.ts +83 -0
  310. package/server/services/platform.ts +16 -131
  311. package/server/services/terminal/pty-manager.ts +66 -313
  312. package/server/services/terminal/pty-utils.ts +176 -0
  313. package/server/services/websocket/file-definition-handlers.ts +165 -0
  314. package/server/services/websocket/file-explorer-handlers.ts +37 -452
  315. package/server/services/websocket/file-search-handlers.ts +291 -0
  316. package/server/services/websocket/file-utils.ts +3 -3
  317. package/server/services/websocket/git-branch-handlers.ts +130 -0
  318. package/server/services/websocket/git-diff-handlers.ts +140 -0
  319. package/server/services/websocket/git-handlers.ts +40 -625
  320. package/server/services/websocket/git-log-handlers.ts +149 -0
  321. package/server/services/websocket/git-pr-handlers.ts +17 -62
  322. package/server/services/websocket/git-tag-handlers.ts +91 -0
  323. package/server/services/websocket/git-utils.ts +230 -0
  324. package/server/services/websocket/handler.ts +39 -112
  325. package/server/services/websocket/plan-board-handlers.ts +277 -0
  326. package/server/services/websocket/plan-execution-handlers.ts +184 -0
  327. package/server/services/websocket/plan-handlers.ts +23 -544
  328. package/server/services/websocket/plan-helpers.ts +215 -0
  329. package/server/services/websocket/plan-issue-handlers.ts +204 -0
  330. package/server/services/websocket/plan-sprint-handlers.ts +252 -0
  331. package/server/services/websocket/quality-complexity.ts +294 -0
  332. package/server/services/websocket/quality-fix-agent.ts +181 -0
  333. package/server/services/websocket/quality-handlers.ts +36 -404
  334. package/server/services/websocket/quality-linting.ts +187 -0
  335. package/server/services/websocket/quality-review-agent.ts +246 -0
  336. package/server/services/websocket/quality-service.ts +11 -762
  337. package/server/services/websocket/quality-tools.ts +209 -0
  338. package/server/services/websocket/quality-types.ts +169 -0
  339. package/server/services/websocket/session-handlers.ts +5 -437
  340. package/server/services/websocket/session-history.ts +222 -0
  341. package/server/services/websocket/session-initialization.ts +209 -0
  342. package/server/services/websocket/types.ts +46 -2
@@ -0,0 +1,215 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ import { mkdirSync, writeFileSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+ import { PlanExecutor } from '../plan/executor.js';
7
+ import { defaultPmDir, resolvePmDir } from '../plan/parser.js';
8
+ import type { Workspace } from '../plan/types.js';
9
+ import { PlanWatcher } from '../plan/watcher.js';
10
+ import type { HandlerContext } from './handler-context.js';
11
+ import type { WSContext } from './types.js';
12
+
13
+ export const watcherCache = new Map<string, PlanWatcher>();
14
+ export const executorCache = new Map<string, PlanExecutor>();
15
+
16
+ /** Validate that a user-supplied path resolves within the .pm/ (or legacy .plan/) directory. */
17
+ export function resolvePlanPath(workingDir: string, relativePath: string): string | null {
18
+ const pmDir = resolvePmDir(workingDir);
19
+ if (!pmDir) return null;
20
+ const resolved = resolve(pmDir, relativePath);
21
+ if (!resolved.startsWith(`${pmDir}/`) && resolved !== pmDir) return null;
22
+ return resolved;
23
+ }
24
+
25
+ /** Guard for write operations — returns true if denied. */
26
+ export function denyIfViewOnly(ctx: HandlerContext, ws: WSContext, permission?: 'control' | 'view'): boolean {
27
+ if (permission === 'view') {
28
+ ctx.send(ws, { type: 'planError', data: { error: 'Permission denied' } });
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+
34
+ export function formatYamlValue(value: unknown): string {
35
+ if (value === null || value === undefined) return 'null';
36
+ if (typeof value === 'boolean') return String(value);
37
+ if (typeof value === 'number') return String(value);
38
+ if (Array.isArray(value)) {
39
+ if (value.length === 0) return '[]';
40
+ return `[${value.map(v => typeof v === 'string' ? v : String(v)).join(', ')}]`;
41
+ }
42
+ return `"${String(value).replace(/"/g, '\\"')}"`;
43
+ }
44
+
45
+ export function buildIssueMarkdown(
46
+ id: string, title: string, type: string, priority: string,
47
+ labels: string[], sprint: string | null, description: string,
48
+ ): string {
49
+ const labelsYaml = labels.length > 0 ? `[${labels.join(', ')}]` : '[]';
50
+ const today = new Date().toISOString().split('T')[0];
51
+ return `---
52
+ id: ${id}
53
+ title: "${title.replace(/"/g, '\\"')}"
54
+ type: ${type}
55
+ status: backlog
56
+ priority: ${priority}
57
+ estimate: null
58
+ labels: ${labelsYaml}
59
+ epic: null
60
+ sprint: ${sprint || 'null'}
61
+ milestone: null
62
+ assigned: null
63
+ created: "${today}"
64
+ due: null
65
+ blocked_by: []
66
+ blocks: []
67
+ relates_to: []
68
+ ---
69
+
70
+ # ${id}: ${title}
71
+
72
+ ## Description
73
+ ${description}
74
+
75
+ ## Acceptance Criteria
76
+
77
+ ## Technical Notes
78
+
79
+ ## Files to Modify
80
+
81
+ ## Activity
82
+ `;
83
+ }
84
+
85
+ function buildProjectMarkdown(name: string): string {
86
+ const today = new Date().toISOString().split('T')[0];
87
+ const projectId = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
88
+ return `---
89
+ name: "${name}"
90
+ id: ${projectId}
91
+ created: "${today}"
92
+ status: active
93
+ estimation: fibonacci
94
+ id_prefixes:
95
+ epic: EP
96
+ issue: IS
97
+ bug: BG
98
+ labels: []
99
+ ---
100
+
101
+ # ${name}
102
+
103
+ ## Goals
104
+
105
+ ## Teams
106
+
107
+ ## Labels
108
+
109
+ ## Workflows
110
+ | Status | Category | Description |
111
+ |---|---|---|
112
+ | backlog | unstarted | Accepted, not yet scheduled |
113
+ | todo | unstarted | Scheduled for current sprint |
114
+ | in_progress | started | Actively being worked on |
115
+ | in_review | started | PR open, awaiting review |
116
+ | done | completed | Merged and verified |
117
+ | cancelled | cancelled | Will not be done |
118
+ `;
119
+ }
120
+
121
+ function buildStateMarkdown(name: string): string {
122
+ return `---
123
+ project: "${name}"
124
+ current_sprint: null
125
+ active_milestone: null
126
+ paused: false
127
+ last_session: null
128
+ ---
129
+
130
+ # Project State
131
+
132
+ ## Current Focus
133
+
134
+ ## Ready to Work
135
+
136
+ ## In Progress
137
+
138
+ ## Blocked
139
+
140
+ ## Recently Completed
141
+
142
+ ## Warnings
143
+ `;
144
+ }
145
+
146
+ export function getWatcher(workingDir: string, ctx: HandlerContext): PlanWatcher {
147
+ let watcher = watcherCache.get(workingDir);
148
+ if (!watcher) {
149
+ watcher = new PlanWatcher(workingDir, ctx);
150
+ watcherCache.set(workingDir, watcher);
151
+ }
152
+ return watcher;
153
+ }
154
+
155
+ export function getExecutor(workingDir: string): PlanExecutor {
156
+ let executor = executorCache.get(workingDir);
157
+ if (!executor) {
158
+ executor = new PlanExecutor(workingDir);
159
+ executorCache.set(workingDir, executor);
160
+ }
161
+ return executor;
162
+ }
163
+
164
+ /** Cleanup watchers and executors for a working directory. */
165
+ export function cleanupPlanResources(workingDir: string): void {
166
+ const watcher = watcherCache.get(workingDir);
167
+ if (watcher) {
168
+ watcher.stop();
169
+ watcherCache.delete(workingDir);
170
+ }
171
+ const executor = executorCache.get(workingDir);
172
+ if (executor) {
173
+ executor.stop();
174
+ executorCache.delete(workingDir);
175
+ }
176
+ }
177
+
178
+ /** Create the .mstro/pm/ directory structure with a default board. */
179
+ export function scaffoldPmDirectory(workingDir: string, name: string): void {
180
+ const planDir = defaultPmDir(workingDir);
181
+ const boardId = 'BOARD-001';
182
+ const boardDir = join(planDir, 'boards', boardId);
183
+
184
+ for (const dir of ['milestones', 'templates']) {
185
+ mkdirSync(join(planDir, dir), { recursive: true });
186
+ }
187
+ for (const dir of ['backlog', 'out', 'reviews', 'logs']) {
188
+ mkdirSync(join(boardDir, dir), { recursive: true });
189
+ }
190
+
191
+ writeFileSync(join(planDir, 'project.md'), buildProjectMarkdown(name), 'utf-8');
192
+
193
+ const workspace: Workspace = { activeBoardId: boardId, boardOrder: [boardId] };
194
+ writeFileSync(join(planDir, 'workspace.json'), JSON.stringify(workspace, null, 2), 'utf-8');
195
+
196
+ const today = new Date().toISOString().split('T')[0];
197
+ writeFileSync(join(boardDir, 'board.md'), `---
198
+ id: ${boardId}
199
+ title: "Board 1"
200
+ status: draft
201
+ created: "${today}"
202
+ completed_at: null
203
+ goal: ""
204
+ ---
205
+
206
+ # Board 1
207
+
208
+ ## Goal
209
+
210
+ ## Notes
211
+ `, 'utf-8');
212
+
213
+ writeFileSync(join(boardDir, 'STATE.md'), buildStateMarkdown(name), 'utf-8');
214
+ writeFileSync(join(boardDir, 'progress.md'), '# Board Progress\n', 'utf-8');
215
+ }
@@ -0,0 +1,204 @@
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, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
5
+ import { basename, join } from 'node:path';
6
+ import { replaceFrontMatterField } from '../plan/front-matter.js';
7
+ import { defaultPmDir, getNextId, parseBoardDirectory, parsePlanDirectory, parseSingleIssue, parseSingleMilestone, parseSingleSprint, planDirExists, resolvePmDir } from '../plan/parser.js';
8
+ import type { HandlerContext } from './handler-context.js';
9
+ import { buildIssueMarkdown, denyIfViewOnly, formatYamlValue, getWatcher, resolvePlanPath, scaffoldPmDirectory } from './plan-helpers.js';
10
+ import type { WebSocketMessage, WSContext } from './types.js';
11
+
12
+ // ============================================================================
13
+ // Read-only handlers
14
+ // ============================================================================
15
+
16
+ export function handlePlanInit(ctx: HandlerContext, ws: WSContext, workingDir: string): void {
17
+ if (!planDirExists(workingDir)) {
18
+ const projectName = basename(workingDir) || 'My Project';
19
+ scaffoldPmDirectory(workingDir, projectName);
20
+ }
21
+
22
+ const fullState = parsePlanDirectory(workingDir);
23
+ if (!fullState) {
24
+ ctx.send(ws, { type: 'planNotFound', data: {} });
25
+ return;
26
+ }
27
+
28
+ ctx.send(ws, { type: 'planState', data: fullState });
29
+
30
+ const watcher = getWatcher(workingDir, ctx);
31
+ watcher.start();
32
+ }
33
+
34
+ export function handleListIssues(ctx: HandlerContext, ws: WSContext, workingDir: string): void {
35
+ const fullState = parsePlanDirectory(workingDir);
36
+ if (!fullState) {
37
+ ctx.send(ws, { type: 'planNotFound', data: {} });
38
+ return;
39
+ }
40
+ ctx.send(ws, { type: 'planIssueList', data: { issues: fullState.issues } });
41
+ }
42
+
43
+ export function handleGetIssue(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, workingDir: string): void {
44
+ const path = msg.data?.path;
45
+ if (!path || !resolvePlanPath(workingDir, path)) {
46
+ ctx.send(ws, { type: 'planError', data: { error: 'Invalid issue path' } });
47
+ return;
48
+ }
49
+ const issue = parseSingleIssue(workingDir, path);
50
+ if (!issue) {
51
+ ctx.send(ws, { type: 'planError', data: { error: `Issue not found: ${path}` } });
52
+ return;
53
+ }
54
+ ctx.send(ws, { type: 'planIssue', data: issue });
55
+ }
56
+
57
+ export function handleGetSprint(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, workingDir: string): void {
58
+ const path = msg.data?.path;
59
+ if (!path || !resolvePlanPath(workingDir, path)) {
60
+ ctx.send(ws, { type: 'planError', data: { error: 'Invalid sprint path' } });
61
+ return;
62
+ }
63
+ const sprint = parseSingleSprint(workingDir, path);
64
+ if (!sprint) {
65
+ ctx.send(ws, { type: 'planError', data: { error: `Sprint not found: ${path}` } });
66
+ return;
67
+ }
68
+ ctx.send(ws, { type: 'planSprint', data: sprint });
69
+ }
70
+
71
+ export function handleGetMilestone(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, workingDir: string): void {
72
+ const path = msg.data?.path;
73
+ if (!path || !resolvePlanPath(workingDir, path)) {
74
+ ctx.send(ws, { type: 'planError', data: { error: 'Invalid milestone path' } });
75
+ return;
76
+ }
77
+ const milestone = parseSingleMilestone(workingDir, path);
78
+ if (!milestone) {
79
+ ctx.send(ws, { type: 'planError', data: { error: `Milestone not found: ${path}` } });
80
+ return;
81
+ }
82
+ ctx.send(ws, { type: 'planMilestone', data: milestone });
83
+ }
84
+
85
+ // ============================================================================
86
+ // Mutation handlers
87
+ // ============================================================================
88
+
89
+ /** Resolve backlog directory and existing issues for a board or legacy layout. */
90
+ function resolveBacklogContext(pmDir: string, workingDir: string, boardId?: string) {
91
+ const fullState = parsePlanDirectory(workingDir);
92
+ const effectiveBoardId = boardId || fullState?.workspace?.activeBoardId;
93
+
94
+ if (effectiveBoardId && existsSync(join(pmDir, 'boards', effectiveBoardId))) {
95
+ const boardState = parseBoardDirectory(pmDir, effectiveBoardId);
96
+ return {
97
+ backlogDir: join(pmDir, 'boards', effectiveBoardId, 'backlog'),
98
+ issues: boardState?.issues ?? [],
99
+ pathPrefix: `boards/${effectiveBoardId}/backlog`,
100
+ };
101
+ }
102
+ return {
103
+ backlogDir: join(pmDir, 'backlog'),
104
+ issues: fullState?.issues ?? [],
105
+ pathPrefix: 'backlog',
106
+ };
107
+ }
108
+
109
+ export function handleCreateIssue(
110
+ ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
111
+ workingDir: string, permission?: 'control' | 'view',
112
+ ): void {
113
+ if (denyIfViewOnly(ctx, ws, permission)) return;
114
+
115
+ const { title, type = 'issue', priority = 'P2', labels = [], sprint, description = '', boardId } = msg.data || {};
116
+ if (!title) {
117
+ ctx.send(ws, { type: 'planError', data: { error: 'Title required' } });
118
+ return;
119
+ }
120
+
121
+ const pmDir = resolvePmDir(workingDir) ?? defaultPmDir(workingDir);
122
+ const { backlogDir, issues, pathPrefix } = resolveBacklogContext(pmDir, workingDir, boardId);
123
+
124
+ if (!existsSync(backlogDir)) mkdirSync(backlogDir, { recursive: true });
125
+
126
+ const prefix = type === 'bug' ? 'BG' : type === 'epic' ? 'EP' : 'IS';
127
+ const id = getNextId(issues, prefix);
128
+ const fileName = `${id}.md`;
129
+
130
+ writeFileSync(join(backlogDir, fileName), buildIssueMarkdown(id, title, type, priority, labels, sprint, description), 'utf-8');
131
+
132
+ const issue = parseSingleIssue(workingDir, `${pathPrefix}/${fileName}`);
133
+ ctx.broadcastToAll({ type: 'planIssueCreated', data: issue });
134
+ }
135
+
136
+ export function handleUpdateIssue(
137
+ ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
138
+ workingDir: string, permission?: 'control' | 'view',
139
+ ): void {
140
+ if (denyIfViewOnly(ctx, ws, permission)) return;
141
+
142
+ const { path, fields } = msg.data || {};
143
+ if (!path || !fields) {
144
+ ctx.send(ws, { type: 'planError', data: { error: 'Path and fields required' } });
145
+ return;
146
+ }
147
+
148
+ const fullPath = resolvePlanPath(workingDir, path);
149
+ if (!fullPath || !existsSync(fullPath)) {
150
+ ctx.send(ws, { type: 'planError', data: { error: `File not found: ${path}` } });
151
+ return;
152
+ }
153
+
154
+ let content = readFileSync(fullPath, 'utf-8');
155
+ if (!content.match(/^---\n/)) {
156
+ ctx.send(ws, { type: 'planError', data: { error: 'Invalid file format' } });
157
+ return;
158
+ }
159
+
160
+ for (const [key, value] of Object.entries(fields as Record<string, unknown>)) {
161
+ const yamlKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
162
+ content = replaceFrontMatterField(content, yamlKey, formatYamlValue(value));
163
+ }
164
+
165
+ writeFileSync(fullPath, content, 'utf-8');
166
+
167
+ const issue = parseSingleIssue(workingDir, path);
168
+ ctx.broadcastToAll({ type: 'planIssueUpdated', data: issue });
169
+ }
170
+
171
+ export function handleDeleteIssue(
172
+ ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
173
+ workingDir: string, permission?: 'control' | 'view',
174
+ ): void {
175
+ if (denyIfViewOnly(ctx, ws, permission)) return;
176
+
177
+ const path = msg.data?.path;
178
+ if (!path) {
179
+ ctx.send(ws, { type: 'planError', data: { error: 'Path required' } });
180
+ return;
181
+ }
182
+
183
+ const fullPath = resolvePlanPath(workingDir, path);
184
+ if (!fullPath || !existsSync(fullPath)) {
185
+ ctx.send(ws, { type: 'planError', data: { error: `File not found: ${path}` } });
186
+ return;
187
+ }
188
+
189
+ unlinkSync(fullPath);
190
+ ctx.broadcastToAll({ type: 'planIssueDeleted', data: { path } });
191
+ }
192
+
193
+ export function handleScaffold(
194
+ ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
195
+ workingDir: string, permission?: 'control' | 'view',
196
+ ): void {
197
+ if (denyIfViewOnly(ctx, ws, permission)) return;
198
+
199
+ const name = msg.data?.name || 'My Project';
200
+ scaffoldPmDirectory(workingDir, name);
201
+
202
+ const fullState = parsePlanDirectory(workingDir);
203
+ ctx.broadcastToAll({ type: 'planScaffolded', data: fullState });
204
+ }
@@ -0,0 +1,252 @@
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, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { replaceFrontMatterField } from '../plan/front-matter.js';
7
+ import { defaultPmDir, getNextSprintId, parsePlanDirectory, parseSingleSprint, parseSprintArtifacts, resolvePmDir } from '../plan/parser.js';
8
+ import type { Issue } from '../plan/types.js';
9
+ import type { HandlerContext } from './handler-context.js';
10
+ import { denyIfViewOnly, resolvePlanPath } from './plan-helpers.js';
11
+ import type { WebSocketMessage, WSContext } from './types.js';
12
+
13
+ // ============================================================================
14
+ // Sprint lifecycle handlers (legacy — kept for backward compatibility)
15
+ // ============================================================================
16
+
17
+ function buildSprintMarkdown(
18
+ id: string, title: string, goal: string, start: string, end: string,
19
+ issueRefs: string[],
20
+ ): string {
21
+ const issuesYaml = issueRefs.length > 0
22
+ ? `\n${issueRefs.map(p => ` - ${p}`).join('\n')}`
23
+ : ' []';
24
+ return `---
25
+ id: ${id}
26
+ title: "${title.replace(/"/g, '\\"')}"
27
+ status: planned
28
+ start: "${start}"
29
+ end: "${end}"
30
+ goal: "${goal.replace(/"/g, '\\"')}"
31
+ capacity: null
32
+ committed: null
33
+ completed: null
34
+ completed_at: null
35
+ issues:${issuesYaml}
36
+ ---
37
+
38
+ # ${id}: ${title}
39
+
40
+ ## Sprint Goal
41
+ ${goal}
42
+
43
+ ## Issues
44
+ | Issue | Title | Points | Status |
45
+ |---|---|---|---|
46
+ `;
47
+ }
48
+
49
+ /** Assign issues to a sprint by updating their front matter sprint field. */
50
+ function assignIssuesToSprint(workingDir: string, issues: Issue[], issueIds: string[], sprintPath: string): void {
51
+ for (const issueId of issueIds) {
52
+ const issue = issues.find(i => i.id === issueId);
53
+ if (!issue) continue;
54
+ const fullPath = resolvePlanPath(workingDir, issue.path);
55
+ if (!fullPath || !existsSync(fullPath)) continue;
56
+ const content = replaceFrontMatterField(readFileSync(fullPath, 'utf-8'), 'sprint', sprintPath);
57
+ writeFileSync(fullPath, content, 'utf-8');
58
+ }
59
+ }
60
+
61
+ export function handleCreateSprint(
62
+ ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
63
+ workingDir: string, permission?: 'control' | 'view',
64
+ ): void {
65
+ if (denyIfViewOnly(ctx, ws, permission)) return;
66
+
67
+ const { title, goal = '', start = '', end = '', issueIds = [] } = msg.data || {};
68
+ if (!title) {
69
+ ctx.send(ws, { type: 'planError', data: { error: 'Sprint title required' } });
70
+ return;
71
+ }
72
+
73
+ const pmDir = resolvePmDir(workingDir) ?? defaultPmDir(workingDir);
74
+ const sprintsDir = join(pmDir, 'sprints');
75
+ if (!existsSync(sprintsDir)) mkdirSync(sprintsDir, { recursive: true });
76
+
77
+ const fullState = parsePlanDirectory(workingDir);
78
+ const id = fullState ? getNextSprintId(fullState.sprints) : 'SPRINT-001';
79
+
80
+ const issueRefs = (issueIds as string[]).map((issueId: string) => {
81
+ const issue = fullState?.issues.find(i => i.id === issueId);
82
+ return issue ? issue.path : `backlog/${issueId}.md`;
83
+ });
84
+
85
+ writeFileSync(join(sprintsDir, `${id}.md`), buildSprintMarkdown(id, title, goal, start, end, issueRefs), 'utf-8');
86
+
87
+ const sandboxDir = join(sprintsDir, id);
88
+ mkdirSync(join(sandboxDir, 'out'), { recursive: true });
89
+ mkdirSync(join(sandboxDir, 'reviews'), { recursive: true });
90
+ writeFileSync(join(sandboxDir, 'progress.md'), `# ${id}: ${title} — Progress Log\n`, 'utf-8');
91
+
92
+ if (issueRefs.length > 0 && fullState) {
93
+ assignIssuesToSprint(workingDir, fullState.issues, issueIds as string[], `sprints/${id}.md`);
94
+ }
95
+
96
+ const sprint = parseSingleSprint(workingDir, `sprints/${id}.md`);
97
+ ctx.broadcastToAll({ type: 'planSprintCreated', data: sprint });
98
+ }
99
+
100
+ /** Promote sprint issues from 'backlog' to 'todo' status. */
101
+ function promoteSprintIssues(pmDir: string, sprint: { issues: Array<{ id: string; path: string }> }, allIssues: Issue[]): void {
102
+ for (const issueSummary of sprint.issues) {
103
+ const issue = allIssues.find(i => i.id === issueSummary.id || i.path === issueSummary.path);
104
+ if (!issue || issue.status !== 'backlog') continue;
105
+ const issuePath = join(pmDir, issue.path);
106
+ if (!existsSync(issuePath)) continue;
107
+ writeFileSync(issuePath, replaceFrontMatterField(readFileSync(issuePath, 'utf-8'), 'status', 'todo'), 'utf-8');
108
+ }
109
+ }
110
+
111
+ /** Update a file's front matter field if the file exists. */
112
+ function updateFileField(filePath: string, field: string, value: string): void {
113
+ if (!existsSync(filePath)) return;
114
+ writeFileSync(filePath, replaceFrontMatterField(readFileSync(filePath, 'utf-8'), field, value), 'utf-8');
115
+ }
116
+
117
+ export function handleActivateSprint(
118
+ ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
119
+ workingDir: string, permission?: 'control' | 'view',
120
+ ): void {
121
+ if (denyIfViewOnly(ctx, ws, permission)) return;
122
+
123
+ const sprintId = msg.data?.sprintId;
124
+ if (!sprintId) {
125
+ ctx.send(ws, { type: 'planError', data: { error: 'Sprint ID required' } });
126
+ return;
127
+ }
128
+
129
+ const fullState = parsePlanDirectory(workingDir);
130
+ if (!fullState) {
131
+ ctx.send(ws, { type: 'planError', data: { error: 'No project found' } });
132
+ return;
133
+ }
134
+
135
+ const currentActive = fullState.sprints.find(s => s.status === 'active');
136
+ if (currentActive && currentActive.id !== sprintId) {
137
+ ctx.send(ws, { type: 'planError', data: { error: `Sprint ${currentActive.id} is already active. Complete it first.` } });
138
+ return;
139
+ }
140
+
141
+ const sprint = fullState.sprints.find(s => s.id === sprintId);
142
+ if (!sprint) {
143
+ ctx.send(ws, { type: 'planError', data: { error: `Sprint not found: ${sprintId}` } });
144
+ return;
145
+ }
146
+
147
+ const pmDir = resolvePmDir(workingDir);
148
+ if (!pmDir) return;
149
+
150
+ updateFileField(join(pmDir, sprint.path), 'status', 'active');
151
+ updateFileField(join(pmDir, 'STATE.md'), 'current_sprint', `"${sprint.path}"`);
152
+ promoteSprintIssues(pmDir, sprint, fullState.issues);
153
+
154
+ const updatedSprint = parseSingleSprint(workingDir, sprint.path);
155
+ ctx.broadcastToAll({ type: 'planSprintUpdated', data: updatedSprint });
156
+
157
+ const updatedState = parsePlanDirectory(workingDir);
158
+ if (updatedState) {
159
+ ctx.broadcastToAll({ type: 'planStateUpdated', data: updatedState });
160
+ }
161
+ }
162
+
163
+ export function handleCompleteSprint(
164
+ ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
165
+ workingDir: string, permission?: 'control' | 'view',
166
+ ): void {
167
+ if (denyIfViewOnly(ctx, ws, permission)) return;
168
+
169
+ const sprintId = msg.data?.sprintId;
170
+ if (!sprintId) {
171
+ ctx.send(ws, { type: 'planError', data: { error: 'Sprint ID required' } });
172
+ return;
173
+ }
174
+
175
+ const fullState = parsePlanDirectory(workingDir);
176
+ if (!fullState) {
177
+ ctx.send(ws, { type: 'planError', data: { error: 'No project found' } });
178
+ return;
179
+ }
180
+
181
+ const sprint = fullState.sprints.find(s => s.id === sprintId);
182
+ if (!sprint) {
183
+ ctx.send(ws, { type: 'planError', data: { error: `Sprint not found: ${sprintId}` } });
184
+ return;
185
+ }
186
+
187
+ const pmDir = resolvePmDir(workingDir);
188
+ if (!pmDir) return;
189
+
190
+ const now = new Date().toISOString();
191
+
192
+ const sprintIssues = fullState.issues.filter(i => i.sprint === sprint.path);
193
+ const completedIssues = sprintIssues.filter(i => i.status === 'done').length;
194
+ const failedIssues = sprintIssues.filter(i => i.status !== 'done' && i.status !== 'cancelled').length;
195
+
196
+ const sprintPath = join(pmDir, sprint.path);
197
+ if (existsSync(sprintPath)) {
198
+ let content = readFileSync(sprintPath, 'utf-8');
199
+ content = replaceFrontMatterField(content, 'status', 'completed');
200
+ content = replaceFrontMatterField(content, 'completed_at', `"${now}"`);
201
+ content = replaceFrontMatterField(content, 'completed', String(completedIssues));
202
+
203
+ if (!content.includes('execution_summary:')) {
204
+ const summaryYaml = [
205
+ 'execution_summary:',
206
+ ` total_issues: ${sprintIssues.length}`,
207
+ ` completed_issues: ${completedIssues}`,
208
+ ` failed_issues: ${failedIssues}`,
209
+ ].join('\n');
210
+ const fmClose = content.indexOf('\n---', content.indexOf('---') + 3);
211
+ if (fmClose !== -1) {
212
+ content = `${content.slice(0, fmClose)}\n${summaryYaml}${content.slice(fmClose)}`;
213
+ }
214
+ }
215
+
216
+ writeFileSync(sprintPath, content, 'utf-8');
217
+ }
218
+
219
+ const statePath = join(pmDir, 'STATE.md');
220
+ if (existsSync(statePath)) {
221
+ let stateContent = readFileSync(statePath, 'utf-8');
222
+ stateContent = replaceFrontMatterField(stateContent, 'current_sprint', 'null');
223
+ writeFileSync(statePath, stateContent, 'utf-8');
224
+ }
225
+
226
+ const updatedSprint = parseSingleSprint(workingDir, sprint.path);
227
+ ctx.broadcastToAll({ type: 'planSprintCompleted', data: updatedSprint });
228
+
229
+ const updatedState = parsePlanDirectory(workingDir);
230
+ if (updatedState) {
231
+ ctx.broadcastToAll({ type: 'planStateUpdated', data: updatedState });
232
+ }
233
+ }
234
+
235
+ export function handleGetSprintArtifacts(
236
+ ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
237
+ workingDir: string,
238
+ ): void {
239
+ const sprintId = msg.data?.sprintId;
240
+ if (!sprintId) {
241
+ ctx.send(ws, { type: 'planError', data: { error: 'Sprint ID required' } });
242
+ return;
243
+ }
244
+
245
+ const artifacts = parseSprintArtifacts(workingDir, sprintId);
246
+ if (!artifacts) {
247
+ ctx.send(ws, { type: 'planSprintArtifacts', data: { sprintId, progressLog: '', outputFiles: [], reviewResults: [] } });
248
+ return;
249
+ }
250
+
251
+ ctx.send(ws, { type: 'planSprintArtifacts', data: artifacts });
252
+ }