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,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
+ }