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
@@ -2,195 +2,21 @@
2
2
  // Licensed under the MIT License. See LICENSE file for details.
3
3
 
4
4
  /**
5
- * Plan Handlers — WebSocket message handlers for Plan view
5
+ * Plan Handlers — WebSocket message router for Plan view
6
6
  *
7
- * Routes plan* messages to the PPS parser and file operations.
7
+ * Routes plan* messages to domain-specific handler modules.
8
8
  * Follows the same pattern as quality-handlers.ts and git-handlers.ts.
9
9
  */
10
10
 
11
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
12
- import { basename, join, resolve } from 'node:path';
13
- import { handlePlanPrompt } from '../plan/composer.js';
14
- import { PlanExecutor } from '../plan/executor.js';
15
- import { replaceFrontMatterField } from '../plan/front-matter.js';
16
- import { defaultPmDir, getNextBoardId, getNextBoardNumber, getNextId, getNextSprintId, parseBoardArtifacts, parseBoardDirectory, parsePlanDirectory, parseSingleIssue, parseSingleMilestone, parseSingleSprint, parseSprintArtifacts, planDirExists, resolvePmDir } from '../plan/parser.js';
17
- import type { Issue, Workspace } from '../plan/types.js';
18
- import { PlanWatcher } from '../plan/watcher.js';
19
11
  import type { HandlerContext } from './handler-context.js';
12
+ import { handleArchiveBoard, handleCreateBoard, handleGetBoard, handleGetBoardArtifacts, handleGetBoardState, handleReorderBoards, handleSetActiveBoard, handleUpdateBoard } from './plan-board-handlers.js';
13
+ import { handleExecute, handleExecuteEpic, handlePause, handlePrompt, handleResume, handleStop } from './plan-execution-handlers.js';
14
+ import { handleCreateIssue, handleDeleteIssue, handleGetIssue, handleGetMilestone, handleGetSprint, handleListIssues, handlePlanInit, handleScaffold, handleUpdateIssue } from './plan-issue-handlers.js';
15
+ import { handleActivateSprint, handleCompleteSprint, handleCreateSprint, handleGetSprintArtifacts } from './plan-sprint-handlers.js';
20
16
  import type { WebSocketMessage, WSContext } from './types.js';
21
17
 
22
- const watcherCache = new Map<string, PlanWatcher>();
23
- const executorCache = new Map<string, PlanExecutor>();
24
-
25
- // ============================================================================
26
- // Helpers
27
- // ============================================================================
28
-
29
- /** Validate that a user-supplied path resolves within the .pm/ (or legacy .plan/) directory. */
30
- function resolvePlanPath(workingDir: string, relativePath: string): string | null {
31
- const pmDir = resolvePmDir(workingDir);
32
- if (!pmDir) return null;
33
- const resolved = resolve(pmDir, relativePath);
34
- if (!resolved.startsWith(`${pmDir}/`) && resolved !== pmDir) return null;
35
- return resolved;
36
- }
37
-
38
- /** Guard for write operations — returns true if denied. */
39
- function denyIfViewOnly(ctx: HandlerContext, ws: WSContext, permission?: 'control' | 'view'): boolean {
40
- if (permission === 'view') {
41
- ctx.send(ws, { type: 'planError', data: { error: 'Permission denied' } });
42
- return true;
43
- }
44
- return false;
45
- }
46
-
47
- function formatYamlValue(value: unknown): string {
48
- if (value === null || value === undefined) return 'null';
49
- if (typeof value === 'boolean') return String(value);
50
- if (typeof value === 'number') return String(value);
51
- if (Array.isArray(value)) {
52
- if (value.length === 0) return '[]';
53
- return `[${value.map(v => typeof v === 'string' ? v : String(v)).join(', ')}]`;
54
- }
55
- return `"${String(value).replace(/"/g, '\\"')}"`;
56
- }
57
-
58
- function buildIssueMarkdown(
59
- id: string, title: string, type: string, priority: string,
60
- labels: string[], sprint: string | null, description: string,
61
- ): string {
62
- const labelsYaml = labels.length > 0 ? `[${labels.join(', ')}]` : '[]';
63
- const today = new Date().toISOString().split('T')[0];
64
- return `---
65
- id: ${id}
66
- title: "${title.replace(/"/g, '\\"')}"
67
- type: ${type}
68
- status: backlog
69
- priority: ${priority}
70
- estimate: null
71
- labels: ${labelsYaml}
72
- epic: null
73
- sprint: ${sprint || 'null'}
74
- milestone: null
75
- assigned: null
76
- created: "${today}"
77
- due: null
78
- blocked_by: []
79
- blocks: []
80
- relates_to: []
81
- ---
82
-
83
- # ${id}: ${title}
84
-
85
- ## Description
86
- ${description}
87
-
88
- ## Acceptance Criteria
89
-
90
- ## Technical Notes
91
-
92
- ## Files to Modify
93
-
94
- ## Activity
95
- `;
96
- }
97
-
98
- function buildProjectMarkdown(name: string): string {
99
- const today = new Date().toISOString().split('T')[0];
100
- const projectId = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
101
- return `---
102
- name: "${name}"
103
- id: ${projectId}
104
- created: "${today}"
105
- status: active
106
- estimation: fibonacci
107
- id_prefixes:
108
- epic: EP
109
- issue: IS
110
- bug: BG
111
- labels: []
112
- ---
113
-
114
- # ${name}
115
-
116
- ## Goals
117
-
118
- ## Teams
119
-
120
- ## Labels
121
-
122
- ## Workflows
123
- | Status | Category | Description |
124
- |---|---|---|
125
- | backlog | unstarted | Accepted, not yet scheduled |
126
- | todo | unstarted | Scheduled for current sprint |
127
- | in_progress | started | Actively being worked on |
128
- | in_review | started | PR open, awaiting review |
129
- | done | completed | Merged and verified |
130
- | cancelled | cancelled | Will not be done |
131
- `;
132
- }
133
-
134
- function buildStateMarkdown(name: string): string {
135
- return `---
136
- project: "${name}"
137
- current_sprint: null
138
- active_milestone: null
139
- paused: false
140
- last_session: null
141
- ---
142
-
143
- # Project State
144
-
145
- ## Current Focus
146
-
147
- ## Ready to Work
148
-
149
- ## In Progress
150
-
151
- ## Blocked
152
-
153
- ## Recently Completed
154
-
155
- ## Warnings
156
- `;
157
- }
158
-
159
- function getWatcher(workingDir: string, ctx: HandlerContext): PlanWatcher {
160
- let watcher = watcherCache.get(workingDir);
161
- if (!watcher) {
162
- watcher = new PlanWatcher(workingDir, ctx);
163
- watcherCache.set(workingDir, watcher);
164
- }
165
- return watcher;
166
- }
167
-
168
- function getExecutor(workingDir: string): PlanExecutor {
169
- let executor = executorCache.get(workingDir);
170
- if (!executor) {
171
- executor = new PlanExecutor(workingDir);
172
- executorCache.set(workingDir, executor);
173
- }
174
- return executor;
175
- }
176
-
177
- /** Cleanup watchers and executors for a working directory. */
178
- export function cleanupPlanResources(workingDir: string): void {
179
- const watcher = watcherCache.get(workingDir);
180
- if (watcher) {
181
- watcher.stop();
182
- watcherCache.delete(workingDir);
183
- }
184
- const executor = executorCache.get(workingDir);
185
- if (executor) {
186
- executor.stop();
187
- executorCache.delete(workingDir);
188
- }
189
- }
190
-
191
- // ============================================================================
192
- // Main dispatcher
193
- // ============================================================================
18
+ // Re-export for backward compatibility
19
+ export { cleanupPlanResources } from './plan-helpers.js';
194
20
 
195
21
  export function handlePlanMessage(
196
22
  ctx: HandlerContext,
@@ -243,935 +69,3 @@ export function handlePlanMessage(
243
69
  ctx.send(ws, { type: 'planError', data: { error: errMsg } });
244
70
  }
245
71
  }
246
-
247
- // ============================================================================
248
- // Read-only handlers
249
- // ============================================================================
250
-
251
- /** Create the .mstro/pm/ directory structure with a default board. */
252
- function scaffoldPmDirectory(workingDir: string, name: string): void {
253
- const planDir = defaultPmDir(workingDir);
254
- const boardId = 'BOARD-001';
255
- const boardDir = join(planDir, 'boards', boardId);
256
-
257
- for (const dir of ['milestones', 'templates']) {
258
- mkdirSync(join(planDir, dir), { recursive: true });
259
- }
260
- for (const dir of ['backlog', 'out', 'reviews']) {
261
- mkdirSync(join(boardDir, dir), { recursive: true });
262
- }
263
-
264
- writeFileSync(join(planDir, 'project.md'), buildProjectMarkdown(name), 'utf-8');
265
-
266
- const workspace: Workspace = { activeBoardId: boardId, boardOrder: [boardId] };
267
- writeFileSync(join(planDir, 'workspace.json'), JSON.stringify(workspace, null, 2), 'utf-8');
268
-
269
- const today = new Date().toISOString().split('T')[0];
270
- writeFileSync(join(boardDir, 'board.md'), `---
271
- id: ${boardId}
272
- title: "Board 1"
273
- status: draft
274
- created: "${today}"
275
- completed_at: null
276
- goal: ""
277
- ---
278
-
279
- # Board 1
280
-
281
- ## Goal
282
-
283
- ## Notes
284
- `, 'utf-8');
285
-
286
- writeFileSync(join(boardDir, 'STATE.md'), buildStateMarkdown(name), 'utf-8');
287
- writeFileSync(join(boardDir, 'progress.md'), '# Board Progress\n', 'utf-8');
288
- }
289
-
290
- function handlePlanInit(ctx: HandlerContext, ws: WSContext, workingDir: string): void {
291
- // Auto-scaffold if .mstro/pm/ doesn't exist
292
- if (!planDirExists(workingDir)) {
293
- const projectName = basename(workingDir) || 'My Project';
294
- scaffoldPmDirectory(workingDir, projectName);
295
- }
296
-
297
- const fullState = parsePlanDirectory(workingDir);
298
- if (!fullState) {
299
- ctx.send(ws, { type: 'planNotFound', data: {} });
300
- return;
301
- }
302
-
303
- ctx.send(ws, { type: 'planState', data: fullState });
304
-
305
- const watcher = getWatcher(workingDir, ctx);
306
- watcher.start();
307
- }
308
-
309
- function handleListIssues(ctx: HandlerContext, ws: WSContext, workingDir: string): void {
310
- const fullState = parsePlanDirectory(workingDir);
311
- if (!fullState) {
312
- ctx.send(ws, { type: 'planNotFound', data: {} });
313
- return;
314
- }
315
- ctx.send(ws, { type: 'planIssueList', data: { issues: fullState.issues } });
316
- }
317
-
318
- function handleGetIssue(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, workingDir: string): void {
319
- const path = msg.data?.path;
320
- if (!path || !resolvePlanPath(workingDir, path)) {
321
- ctx.send(ws, { type: 'planError', data: { error: 'Invalid issue path' } });
322
- return;
323
- }
324
- const issue = parseSingleIssue(workingDir, path);
325
- if (!issue) {
326
- ctx.send(ws, { type: 'planError', data: { error: `Issue not found: ${path}` } });
327
- return;
328
- }
329
- ctx.send(ws, { type: 'planIssue', data: issue });
330
- }
331
-
332
- function handleGetSprint(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, workingDir: string): void {
333
- const path = msg.data?.path;
334
- if (!path || !resolvePlanPath(workingDir, path)) {
335
- ctx.send(ws, { type: 'planError', data: { error: 'Invalid sprint path' } });
336
- return;
337
- }
338
- const sprint = parseSingleSprint(workingDir, path);
339
- if (!sprint) {
340
- ctx.send(ws, { type: 'planError', data: { error: `Sprint not found: ${path}` } });
341
- return;
342
- }
343
- ctx.send(ws, { type: 'planSprint', data: sprint });
344
- }
345
-
346
- function handleGetMilestone(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, workingDir: string): void {
347
- const path = msg.data?.path;
348
- if (!path || !resolvePlanPath(workingDir, path)) {
349
- ctx.send(ws, { type: 'planError', data: { error: 'Invalid milestone path' } });
350
- return;
351
- }
352
- const milestone = parseSingleMilestone(workingDir, path);
353
- if (!milestone) {
354
- ctx.send(ws, { type: 'planError', data: { error: `Milestone not found: ${path}` } });
355
- return;
356
- }
357
- ctx.send(ws, { type: 'planMilestone', data: milestone });
358
- }
359
-
360
- // ============================================================================
361
- // Mutation handlers
362
- // ============================================================================
363
-
364
- /** Resolve backlog directory and existing issues for a board or legacy layout. */
365
- function resolveBacklogContext(pmDir: string, workingDir: string, boardId?: string) {
366
- const fullState = parsePlanDirectory(workingDir);
367
- const effectiveBoardId = boardId || fullState?.workspace?.activeBoardId;
368
-
369
- if (effectiveBoardId && existsSync(join(pmDir, 'boards', effectiveBoardId))) {
370
- const boardState = parseBoardDirectory(pmDir, effectiveBoardId);
371
- return {
372
- backlogDir: join(pmDir, 'boards', effectiveBoardId, 'backlog'),
373
- issues: boardState?.issues ?? [],
374
- pathPrefix: `boards/${effectiveBoardId}/backlog`,
375
- };
376
- }
377
- return {
378
- backlogDir: join(pmDir, 'backlog'),
379
- issues: fullState?.issues ?? [],
380
- pathPrefix: 'backlog',
381
- };
382
- }
383
-
384
- function handleCreateIssue(
385
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
386
- workingDir: string, permission?: 'control' | 'view',
387
- ): void {
388
- if (denyIfViewOnly(ctx, ws, permission)) return;
389
-
390
- const { title, type = 'issue', priority = 'P2', labels = [], sprint, description = '', boardId } = msg.data || {};
391
- if (!title) {
392
- ctx.send(ws, { type: 'planError', data: { error: 'Title required' } });
393
- return;
394
- }
395
-
396
- const pmDir = resolvePmDir(workingDir) ?? defaultPmDir(workingDir);
397
- const { backlogDir, issues, pathPrefix } = resolveBacklogContext(pmDir, workingDir, boardId);
398
-
399
- if (!existsSync(backlogDir)) mkdirSync(backlogDir, { recursive: true });
400
-
401
- const prefix = type === 'bug' ? 'BG' : type === 'epic' ? 'EP' : 'IS';
402
- const id = getNextId(issues, prefix);
403
- const fileName = `${id}.md`;
404
-
405
- writeFileSync(join(backlogDir, fileName), buildIssueMarkdown(id, title, type, priority, labels, sprint, description), 'utf-8');
406
-
407
- const issue = parseSingleIssue(workingDir, `${pathPrefix}/${fileName}`);
408
- ctx.broadcastToAll({ type: 'planIssueCreated', data: issue });
409
- }
410
-
411
- function handleUpdateIssue(
412
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
413
- workingDir: string, permission?: 'control' | 'view',
414
- ): void {
415
- if (denyIfViewOnly(ctx, ws, permission)) return;
416
-
417
- const { path, fields } = msg.data || {};
418
- if (!path || !fields) {
419
- ctx.send(ws, { type: 'planError', data: { error: 'Path and fields required' } });
420
- return;
421
- }
422
-
423
- const fullPath = resolvePlanPath(workingDir, path);
424
- if (!fullPath || !existsSync(fullPath)) {
425
- ctx.send(ws, { type: 'planError', data: { error: `File not found: ${path}` } });
426
- return;
427
- }
428
-
429
- let content = readFileSync(fullPath, 'utf-8');
430
- if (!content.match(/^---\n/)) {
431
- ctx.send(ws, { type: 'planError', data: { error: 'Invalid file format' } });
432
- return;
433
- }
434
-
435
- for (const [key, value] of Object.entries(fields as Record<string, unknown>)) {
436
- const yamlKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
437
- content = replaceFrontMatterField(content, yamlKey, formatYamlValue(value));
438
- }
439
-
440
- writeFileSync(fullPath, content, 'utf-8');
441
-
442
- const issue = parseSingleIssue(workingDir, path);
443
- ctx.broadcastToAll({ type: 'planIssueUpdated', data: issue });
444
- }
445
-
446
- function handleDeleteIssue(
447
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
448
- workingDir: string, permission?: 'control' | 'view',
449
- ): void {
450
- if (denyIfViewOnly(ctx, ws, permission)) return;
451
-
452
- const path = msg.data?.path;
453
- if (!path) {
454
- ctx.send(ws, { type: 'planError', data: { error: 'Path required' } });
455
- return;
456
- }
457
-
458
- const fullPath = resolvePlanPath(workingDir, path);
459
- if (!fullPath || !existsSync(fullPath)) {
460
- ctx.send(ws, { type: 'planError', data: { error: `File not found: ${path}` } });
461
- return;
462
- }
463
-
464
- unlinkSync(fullPath);
465
- ctx.broadcastToAll({ type: 'planIssueDeleted', data: { path } });
466
- }
467
-
468
- function handleScaffold(
469
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
470
- workingDir: string, permission?: 'control' | 'view',
471
- ): void {
472
- if (denyIfViewOnly(ctx, ws, permission)) return;
473
-
474
- const name = msg.data?.name || 'My Project';
475
- scaffoldPmDirectory(workingDir, name);
476
-
477
- const fullState = parsePlanDirectory(workingDir);
478
- ctx.broadcastToAll({ type: 'planScaffolded', data: fullState });
479
- }
480
-
481
- // ============================================================================
482
- // Composer + Execution handlers
483
- // ============================================================================
484
-
485
- function handlePrompt(
486
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
487
- workingDir: string, permission?: 'control' | 'view',
488
- ): void {
489
- if (denyIfViewOnly(ctx, ws, permission)) return;
490
-
491
- const prompt = msg.data?.prompt;
492
- const boardId = msg.data?.boardId as string | undefined;
493
- if (!prompt) {
494
- ctx.send(ws, { type: 'planError', data: { error: 'Prompt required' } });
495
- return;
496
- }
497
- handlePlanPrompt(ctx, ws, prompt, workingDir, boardId).catch(error => {
498
- ctx.send(ws, {
499
- type: 'planError',
500
- data: { error: error instanceof Error ? error.message : String(error) },
501
- });
502
- });
503
- }
504
-
505
- function wireExecutorEvents(executor: PlanExecutor, ctx: HandlerContext, workingDir: string): void {
506
- executor.removeAllListeners();
507
-
508
- executor.on('statusChanged', (status: string) => {
509
- ctx.broadcastToAll({ type: 'planExecutionProgress', data: { status } });
510
- });
511
-
512
- executor.on('issueStarted', (issue: { id: string; title: string }) => {
513
- ctx.broadcastToAll({
514
- type: 'planExecutionProgress',
515
- data: { issueId: issue.id, status: 'executing', title: issue.title },
516
- });
517
- });
518
-
519
- executor.on('output', (data: { issueId: string; text: string }) => {
520
- ctx.broadcastToAll({ type: 'planExecutionOutput', data });
521
- });
522
-
523
- executor.on('issueCompleted', () => {
524
- ctx.broadcastToAll({ type: 'planExecutionMetrics', data: executor.getMetrics() });
525
- const fullState = parsePlanDirectory(workingDir);
526
- if (fullState) {
527
- ctx.broadcastToAll({ type: 'planStateUpdated', data: fullState });
528
- }
529
- });
530
-
531
- executor.on('issueError', (data: { issueId: string; error: string }) => {
532
- ctx.broadcastToAll({ type: 'planExecutionError', data });
533
- });
534
-
535
- executor.on('waveStarted', (data: { issueIds: string[] }) => {
536
- ctx.broadcastToAll({
537
- type: 'planExecutionProgress',
538
- data: { status: 'wave', issueIds: data.issueIds },
539
- });
540
- });
541
-
542
- executor.on('waveError', (data: { issueIds: string[]; error: string }) => {
543
- ctx.broadcastToAll({ type: 'planExecutionError', data });
544
- });
545
-
546
- executor.on('stateUpdated', () => {
547
- const fullState = parsePlanDirectory(workingDir);
548
- if (fullState) {
549
- ctx.broadcastToAll({ type: 'planStateUpdated', data: fullState });
550
- }
551
- });
552
-
553
- executor.on('reviewProgress', (data: { issueId: string; status: string }) => {
554
- ctx.broadcastToAll({ type: 'planReviewProgress', data });
555
- });
556
-
557
- executor.on('complete', (reason: string) => {
558
- ctx.broadcastToAll({ type: 'planExecutionComplete', data: { reason, metrics: executor.getMetrics() } });
559
- });
560
-
561
- executor.on('error', (error: string) => {
562
- ctx.broadcastToAll({ type: 'planExecutionError', data: { error } });
563
- });
564
- }
565
-
566
- function handleExecute(
567
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
568
- workingDir: string, permission?: 'control' | 'view',
569
- ): void {
570
- if (denyIfViewOnly(ctx, ws, permission)) return;
571
-
572
- const executor = getExecutor(workingDir);
573
-
574
- if (executor.getStatus() === 'executing' || executor.getStatus() === 'starting') {
575
- ctx.send(ws, { type: 'planError', data: { error: 'Execution already in progress' } });
576
- return;
577
- }
578
-
579
- wireExecutorEvents(executor, ctx, workingDir);
580
-
581
- // Execute the board the user is looking at, falling back to workspace.json activeBoardId
582
- const boardId = msg.data?.boardId as string | undefined;
583
- ctx.send(ws, { type: 'planExecutionStarted', data: { status: 'executing', boardId } });
584
- const startPromise = boardId ? executor.startBoard(boardId) : executor.start();
585
- startPromise.catch(error => {
586
- ctx.send(ws, {
587
- type: 'planExecutionError',
588
- data: { error: error instanceof Error ? error.message : String(error) },
589
- });
590
- });
591
- }
592
-
593
- function handleExecuteEpic(
594
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
595
- workingDir: string, permission?: 'control' | 'view',
596
- ): void {
597
- if (denyIfViewOnly(ctx, ws, permission)) return;
598
-
599
- const epicPath = msg.data?.epicPath;
600
- if (!epicPath) {
601
- ctx.send(ws, { type: 'planError', data: { error: 'Epic path required' } });
602
- return;
603
- }
604
-
605
- const executor = getExecutor(workingDir);
606
-
607
- if (executor.getStatus() === 'executing' || executor.getStatus() === 'starting') {
608
- ctx.send(ws, { type: 'planError', data: { error: 'Execution already in progress' } });
609
- return;
610
- }
611
-
612
- wireExecutorEvents(executor, ctx, workingDir);
613
-
614
- ctx.send(ws, { type: 'planExecutionStarted', data: { status: 'executing', epicPath } });
615
- executor.startEpic(epicPath).catch(error => {
616
- ctx.send(ws, {
617
- type: 'planExecutionError',
618
- data: { error: error instanceof Error ? error.message : String(error) },
619
- });
620
- });
621
- }
622
-
623
- function handlePause(
624
- ctx: HandlerContext, ws: WSContext,
625
- workingDir: string, permission?: 'control' | 'view',
626
- ): void {
627
- if (denyIfViewOnly(ctx, ws, permission)) return;
628
- const executor = executorCache.get(workingDir);
629
- if (executor) executor.pause();
630
- }
631
-
632
- function handleStop(
633
- ctx: HandlerContext, ws: WSContext,
634
- workingDir: string, permission?: 'control' | 'view',
635
- ): void {
636
- if (denyIfViewOnly(ctx, ws, permission)) return;
637
- const executor = executorCache.get(workingDir);
638
- if (executor) executor.stop();
639
- }
640
-
641
- function handleResume(
642
- ctx: HandlerContext, ws: WSContext,
643
- workingDir: string, permission?: 'control' | 'view',
644
- ): void {
645
- if (denyIfViewOnly(ctx, ws, permission)) return;
646
- const executor = executorCache.get(workingDir);
647
- if (executor) {
648
- executor.resume().catch(error => {
649
- ctx.send(ws, {
650
- type: 'planExecutionError',
651
- data: { error: error instanceof Error ? error.message : String(error) },
652
- });
653
- });
654
- }
655
- }
656
-
657
- // ============================================================================
658
- // Board lifecycle handlers
659
- // ============================================================================
660
-
661
- function handleCreateBoard(
662
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
663
- workingDir: string, permission?: 'control' | 'view',
664
- ): void {
665
- if (denyIfViewOnly(ctx, ws, permission)) return;
666
-
667
- const pmDir = resolvePmDir(workingDir);
668
- if (!pmDir) {
669
- ctx.send(ws, { type: 'planError', data: { error: 'No PM directory found' } });
670
- return;
671
- }
672
-
673
- const fullState = parsePlanDirectory(workingDir);
674
- if (!fullState) return;
675
-
676
- const boardId = getNextBoardId(fullState.boards);
677
- const boardNum = getNextBoardNumber(fullState.boards);
678
- const title = msg.data?.title || `Board ${boardNum}`;
679
- const goal = msg.data?.goal || '';
680
- const boardDir = join(pmDir, 'boards', boardId);
681
-
682
- // Create directory structure
683
- for (const dir of ['backlog', 'out', 'reviews']) {
684
- mkdirSync(join(boardDir, dir), { recursive: true });
685
- }
686
-
687
- // Create board.md
688
- const today = new Date().toISOString().split('T')[0];
689
- writeFileSync(join(boardDir, 'board.md'), `---
690
- id: ${boardId}
691
- title: "${title.replace(/"/g, '\\"')}"
692
- status: draft
693
- created: "${today}"
694
- completed_at: null
695
- goal: "${goal.replace(/"/g, '\\"')}"
696
- ---
697
-
698
- # ${title}
699
-
700
- ## Goal
701
- ${goal}
702
-
703
- ## Notes
704
- `, 'utf-8');
705
-
706
- // Create STATE.md
707
- writeFileSync(join(boardDir, 'STATE.md'), `---
708
- project: ../../project.md
709
- board: board.md
710
- paused: false
711
- ---
712
-
713
- # Board State
714
-
715
- ## Ready to Work
716
-
717
- ## In Progress
718
-
719
- ## Blocked
720
-
721
- ## Recently Completed
722
-
723
- ## Warnings
724
- `, 'utf-8');
725
-
726
- // Create progress.md
727
- writeFileSync(join(boardDir, 'progress.md'), '# Board Progress\n', 'utf-8');
728
-
729
- // Update workspace.json
730
- const wsPath = join(pmDir, 'workspace.json');
731
- if (!existsSync(wsPath)) {
732
- writeFileSync(wsPath, JSON.stringify({ activeBoardId: null, boardOrder: [] }, null, 2), 'utf-8');
733
- }
734
- const workspaceContent = readFileSync(wsPath, 'utf-8');
735
- const workspace: Workspace = JSON.parse(workspaceContent);
736
- workspace.boardOrder.push(boardId);
737
- if (!workspace.activeBoardId) {
738
- workspace.activeBoardId = boardId;
739
- }
740
- writeFileSync(join(pmDir, 'workspace.json'), JSON.stringify(workspace, null, 2), 'utf-8');
741
-
742
- const boardState = parseBoardDirectory(pmDir, boardId);
743
- if (boardState) {
744
- ctx.broadcastToAll({ type: 'planBoardCreated', data: boardState.board });
745
- ctx.broadcastToAll({ type: 'planWorkspaceUpdated', data: workspace });
746
- }
747
- }
748
-
749
- function handleUpdateBoard(
750
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
751
- workingDir: string, permission?: 'control' | 'view',
752
- ): void {
753
- if (denyIfViewOnly(ctx, ws, permission)) return;
754
-
755
- const { boardId, fields } = msg.data || {};
756
- if (!boardId || !fields) {
757
- ctx.send(ws, { type: 'planError', data: { error: 'Board ID and fields required' } });
758
- return;
759
- }
760
-
761
- const pmDir = resolvePmDir(workingDir);
762
- if (!pmDir) return;
763
-
764
- const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
765
- if (!existsSync(boardMdPath)) {
766
- ctx.send(ws, { type: 'planError', data: { error: `Board not found: ${boardId}` } });
767
- return;
768
- }
769
-
770
- let content = readFileSync(boardMdPath, 'utf-8');
771
- for (const [key, value] of Object.entries(fields as Record<string, unknown>)) {
772
- const yamlKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
773
- content = replaceFrontMatterField(content, yamlKey, formatYamlValue(value));
774
- }
775
- writeFileSync(boardMdPath, content, 'utf-8');
776
-
777
- const boardState = parseBoardDirectory(pmDir, boardId);
778
- if (boardState) {
779
- ctx.broadcastToAll({ type: 'planBoardUpdated', data: boardState.board });
780
- }
781
- }
782
-
783
- function handleArchiveBoard(
784
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
785
- workingDir: string, permission?: 'control' | 'view',
786
- ): void {
787
- if (denyIfViewOnly(ctx, ws, permission)) return;
788
-
789
- const boardId = msg.data?.boardId;
790
- if (!boardId) {
791
- ctx.send(ws, { type: 'planError', data: { error: 'Board ID required' } });
792
- return;
793
- }
794
-
795
- const pmDir = resolvePmDir(workingDir);
796
- if (!pmDir) return;
797
-
798
- const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
799
- if (!existsSync(boardMdPath)) {
800
- ctx.send(ws, { type: 'planError', data: { error: `Board not found: ${boardId}` } });
801
- return;
802
- }
803
-
804
- // Set status to archived
805
- let content = readFileSync(boardMdPath, 'utf-8');
806
- content = replaceFrontMatterField(content, 'status', 'archived');
807
- writeFileSync(boardMdPath, content, 'utf-8');
808
-
809
- // Remove from workspace.json boardOrder and update activeBoardId if needed
810
- const workspacePath = join(pmDir, 'workspace.json');
811
- if (existsSync(workspacePath)) {
812
- const workspace: Workspace = JSON.parse(readFileSync(workspacePath, 'utf-8'));
813
- workspace.boardOrder = workspace.boardOrder.filter(id => id !== boardId);
814
- if (workspace.activeBoardId === boardId) {
815
- workspace.activeBoardId = workspace.boardOrder[0] || null;
816
- }
817
- writeFileSync(workspacePath, JSON.stringify(workspace, null, 2), 'utf-8');
818
- ctx.broadcastToAll({ type: 'planWorkspaceUpdated', data: workspace });
819
- }
820
-
821
- const boardState = parseBoardDirectory(pmDir, boardId);
822
- if (boardState) {
823
- ctx.broadcastToAll({ type: 'planBoardArchived', data: boardState.board });
824
- }
825
- }
826
-
827
- function handleGetBoard(
828
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
829
- workingDir: string,
830
- ): void {
831
- const boardId = msg.data?.boardId;
832
- if (!boardId) {
833
- ctx.send(ws, { type: 'planError', data: { error: 'Board ID required' } });
834
- return;
835
- }
836
-
837
- const pmDir = resolvePmDir(workingDir);
838
- if (!pmDir) return;
839
-
840
- const boardState = parseBoardDirectory(pmDir, boardId);
841
- if (!boardState) {
842
- ctx.send(ws, { type: 'planError', data: { error: `Board not found: ${boardId}` } });
843
- return;
844
- }
845
-
846
- ctx.send(ws, { type: 'planBoardState', data: boardState });
847
- }
848
-
849
- function handleGetBoardState(
850
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
851
- workingDir: string,
852
- ): void {
853
- handleGetBoard(ctx, ws, msg, workingDir);
854
- }
855
-
856
- function handleReorderBoards(
857
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
858
- workingDir: string, permission?: 'control' | 'view',
859
- ): void {
860
- if (denyIfViewOnly(ctx, ws, permission)) return;
861
-
862
- const boardOrder = msg.data?.boardOrder;
863
- if (!Array.isArray(boardOrder)) {
864
- ctx.send(ws, { type: 'planError', data: { error: 'boardOrder array required' } });
865
- return;
866
- }
867
-
868
- const pmDir = resolvePmDir(workingDir);
869
- if (!pmDir) return;
870
-
871
- const workspacePath = join(pmDir, 'workspace.json');
872
- if (!existsSync(workspacePath)) return;
873
-
874
- const workspace: Workspace = JSON.parse(readFileSync(workspacePath, 'utf-8'));
875
- workspace.boardOrder = boardOrder;
876
- writeFileSync(workspacePath, JSON.stringify(workspace, null, 2), 'utf-8');
877
-
878
- ctx.broadcastToAll({ type: 'planWorkspaceUpdated', data: workspace });
879
- }
880
-
881
- function handleSetActiveBoard(
882
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
883
- workingDir: string, permission?: 'control' | 'view',
884
- ): void {
885
- if (denyIfViewOnly(ctx, ws, permission)) return;
886
-
887
- const boardId = msg.data?.boardId;
888
- if (!boardId) {
889
- ctx.send(ws, { type: 'planError', data: { error: 'Board ID required' } });
890
- return;
891
- }
892
-
893
- const pmDir = resolvePmDir(workingDir);
894
- if (!pmDir) return;
895
-
896
- const workspacePath = join(pmDir, 'workspace.json');
897
- if (!existsSync(workspacePath)) return;
898
-
899
- const workspace: Workspace = JSON.parse(readFileSync(workspacePath, 'utf-8'));
900
- workspace.activeBoardId = boardId;
901
- writeFileSync(workspacePath, JSON.stringify(workspace, null, 2), 'utf-8');
902
-
903
- ctx.broadcastToAll({ type: 'planWorkspaceUpdated', data: workspace });
904
-
905
- // Also send the active board's full state
906
- const boardState = parseBoardDirectory(pmDir, boardId);
907
- if (boardState) {
908
- ctx.send(ws, { type: 'planBoardState', data: boardState });
909
- }
910
- }
911
-
912
- function handleGetBoardArtifacts(
913
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
914
- workingDir: string,
915
- ): void {
916
- const boardId = msg.data?.boardId;
917
- if (!boardId) {
918
- ctx.send(ws, { type: 'planError', data: { error: 'Board ID required' } });
919
- return;
920
- }
921
-
922
- const artifacts = parseBoardArtifacts(workingDir, boardId);
923
- if (!artifacts) {
924
- ctx.send(ws, { type: 'planBoardArtifacts', data: { boardId, progressLog: '', outputFiles: [], reviewResults: [] } });
925
- return;
926
- }
927
-
928
- ctx.send(ws, { type: 'planBoardArtifacts', data: artifacts });
929
- }
930
-
931
- // ============================================================================
932
- // Sprint lifecycle handlers (legacy — kept for backward compatibility)
933
- // ============================================================================
934
-
935
- function buildSprintMarkdown(
936
- id: string, title: string, goal: string, start: string, end: string,
937
- issueRefs: string[],
938
- ): string {
939
- const issuesYaml = issueRefs.length > 0
940
- ? `\n${issueRefs.map(p => ` - ${p}`).join('\n')}`
941
- : ' []';
942
- return `---
943
- id: ${id}
944
- title: "${title.replace(/"/g, '\\"')}"
945
- status: planned
946
- start: "${start}"
947
- end: "${end}"
948
- goal: "${goal.replace(/"/g, '\\"')}"
949
- capacity: null
950
- committed: null
951
- completed: null
952
- completed_at: null
953
- issues:${issuesYaml}
954
- ---
955
-
956
- # ${id}: ${title}
957
-
958
- ## Sprint Goal
959
- ${goal}
960
-
961
- ## Issues
962
- | Issue | Title | Points | Status |
963
- |---|---|---|---|
964
- `;
965
- }
966
-
967
- /** Assign issues to a sprint by updating their front matter sprint field. */
968
- function assignIssuesToSprint(workingDir: string, issues: Issue[], issueIds: string[], sprintPath: string): void {
969
- for (const issueId of issueIds) {
970
- const issue = issues.find(i => i.id === issueId);
971
- if (!issue) continue;
972
- const fullPath = resolvePlanPath(workingDir, issue.path);
973
- if (!fullPath || !existsSync(fullPath)) continue;
974
- const content = replaceFrontMatterField(readFileSync(fullPath, 'utf-8'), 'sprint', sprintPath);
975
- writeFileSync(fullPath, content, 'utf-8');
976
- }
977
- }
978
-
979
- function handleCreateSprint(
980
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
981
- workingDir: string, permission?: 'control' | 'view',
982
- ): void {
983
- if (denyIfViewOnly(ctx, ws, permission)) return;
984
-
985
- const { title, goal = '', start = '', end = '', issueIds = [] } = msg.data || {};
986
- if (!title) {
987
- ctx.send(ws, { type: 'planError', data: { error: 'Sprint title required' } });
988
- return;
989
- }
990
-
991
- const pmDir = resolvePmDir(workingDir) ?? defaultPmDir(workingDir);
992
- const sprintsDir = join(pmDir, 'sprints');
993
- if (!existsSync(sprintsDir)) mkdirSync(sprintsDir, { recursive: true });
994
-
995
- const fullState = parsePlanDirectory(workingDir);
996
- const id = fullState ? getNextSprintId(fullState.sprints) : 'SPRINT-001';
997
-
998
- const issueRefs = (issueIds as string[]).map((issueId: string) => {
999
- const issue = fullState?.issues.find(i => i.id === issueId);
1000
- return issue ? issue.path : `backlog/${issueId}.md`;
1001
- });
1002
-
1003
- writeFileSync(join(sprintsDir, `${id}.md`), buildSprintMarkdown(id, title, goal, start, end, issueRefs), 'utf-8');
1004
-
1005
- const sandboxDir = join(sprintsDir, id);
1006
- mkdirSync(join(sandboxDir, 'out'), { recursive: true });
1007
- mkdirSync(join(sandboxDir, 'reviews'), { recursive: true });
1008
- writeFileSync(join(sandboxDir, 'progress.md'), `# ${id}: ${title} — Progress Log\n`, 'utf-8');
1009
-
1010
- if (issueRefs.length > 0 && fullState) {
1011
- assignIssuesToSprint(workingDir, fullState.issues, issueIds as string[], `sprints/${id}.md`);
1012
- }
1013
-
1014
- const sprint = parseSingleSprint(workingDir, `sprints/${id}.md`);
1015
- ctx.broadcastToAll({ type: 'planSprintCreated', data: sprint });
1016
- }
1017
-
1018
- /** Promote sprint issues from 'backlog' to 'todo' status. */
1019
- function promoteSprintIssues(pmDir: string, sprint: { issues: Array<{ id: string; path: string }> }, allIssues: Issue[]): void {
1020
- for (const issueSummary of sprint.issues) {
1021
- const issue = allIssues.find(i => i.id === issueSummary.id || i.path === issueSummary.path);
1022
- if (!issue || issue.status !== 'backlog') continue;
1023
- const issuePath = join(pmDir, issue.path);
1024
- if (!existsSync(issuePath)) continue;
1025
- writeFileSync(issuePath, replaceFrontMatterField(readFileSync(issuePath, 'utf-8'), 'status', 'todo'), 'utf-8');
1026
- }
1027
- }
1028
-
1029
- /** Update a file's front matter field if the file exists. */
1030
- function updateFileField(filePath: string, field: string, value: string): void {
1031
- if (!existsSync(filePath)) return;
1032
- writeFileSync(filePath, replaceFrontMatterField(readFileSync(filePath, 'utf-8'), field, value), 'utf-8');
1033
- }
1034
-
1035
- function handleActivateSprint(
1036
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
1037
- workingDir: string, permission?: 'control' | 'view',
1038
- ): void {
1039
- if (denyIfViewOnly(ctx, ws, permission)) return;
1040
-
1041
- const sprintId = msg.data?.sprintId;
1042
- if (!sprintId) {
1043
- ctx.send(ws, { type: 'planError', data: { error: 'Sprint ID required' } });
1044
- return;
1045
- }
1046
-
1047
- const fullState = parsePlanDirectory(workingDir);
1048
- if (!fullState) {
1049
- ctx.send(ws, { type: 'planError', data: { error: 'No project found' } });
1050
- return;
1051
- }
1052
-
1053
- const currentActive = fullState.sprints.find(s => s.status === 'active');
1054
- if (currentActive && currentActive.id !== sprintId) {
1055
- ctx.send(ws, { type: 'planError', data: { error: `Sprint ${currentActive.id} is already active. Complete it first.` } });
1056
- return;
1057
- }
1058
-
1059
- const sprint = fullState.sprints.find(s => s.id === sprintId);
1060
- if (!sprint) {
1061
- ctx.send(ws, { type: 'planError', data: { error: `Sprint not found: ${sprintId}` } });
1062
- return;
1063
- }
1064
-
1065
- const pmDir = resolvePmDir(workingDir);
1066
- if (!pmDir) return;
1067
-
1068
- updateFileField(join(pmDir, sprint.path), 'status', 'active');
1069
- updateFileField(join(pmDir, 'STATE.md'), 'current_sprint', `"${sprint.path}"`);
1070
- promoteSprintIssues(pmDir, sprint, fullState.issues);
1071
-
1072
- const updatedSprint = parseSingleSprint(workingDir, sprint.path);
1073
- ctx.broadcastToAll({ type: 'planSprintUpdated', data: updatedSprint });
1074
-
1075
- const updatedState = parsePlanDirectory(workingDir);
1076
- if (updatedState) {
1077
- ctx.broadcastToAll({ type: 'planStateUpdated', data: updatedState });
1078
- }
1079
- }
1080
-
1081
- function handleCompleteSprint(
1082
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
1083
- workingDir: string, permission?: 'control' | 'view',
1084
- ): void {
1085
- if (denyIfViewOnly(ctx, ws, permission)) return;
1086
-
1087
- const sprintId = msg.data?.sprintId;
1088
- if (!sprintId) {
1089
- ctx.send(ws, { type: 'planError', data: { error: 'Sprint ID required' } });
1090
- return;
1091
- }
1092
-
1093
- const fullState = parsePlanDirectory(workingDir);
1094
- if (!fullState) {
1095
- ctx.send(ws, { type: 'planError', data: { error: 'No project found' } });
1096
- return;
1097
- }
1098
-
1099
- const sprint = fullState.sprints.find(s => s.id === sprintId);
1100
- if (!sprint) {
1101
- ctx.send(ws, { type: 'planError', data: { error: `Sprint not found: ${sprintId}` } });
1102
- return;
1103
- }
1104
-
1105
- const pmDir = resolvePmDir(workingDir);
1106
- if (!pmDir) return;
1107
-
1108
- const now = new Date().toISOString();
1109
-
1110
- // Compute execution summary from sprint issues
1111
- const sprintIssues = fullState.issues.filter(i => i.sprint === sprint.path);
1112
- const completedIssues = sprintIssues.filter(i => i.status === 'done').length;
1113
- const failedIssues = sprintIssues.filter(i => i.status !== 'done' && i.status !== 'cancelled').length;
1114
-
1115
- // Update sprint file with completion data
1116
- const sprintPath = join(pmDir, sprint.path);
1117
- if (existsSync(sprintPath)) {
1118
- let content = readFileSync(sprintPath, 'utf-8');
1119
- content = replaceFrontMatterField(content, 'status', 'completed');
1120
- content = replaceFrontMatterField(content, 'completed_at', `"${now}"`);
1121
- content = replaceFrontMatterField(content, 'completed', String(completedIssues));
1122
-
1123
- // Write execution summary if not already present
1124
- if (!content.includes('execution_summary:')) {
1125
- const summaryYaml = [
1126
- 'execution_summary:',
1127
- ` total_issues: ${sprintIssues.length}`,
1128
- ` completed_issues: ${completedIssues}`,
1129
- ` failed_issues: ${failedIssues}`,
1130
- ].join('\n');
1131
- // Insert before the closing --- of front matter (second occurrence)
1132
- const fmClose = content.indexOf('\n---', content.indexOf('---') + 3);
1133
- if (fmClose !== -1) {
1134
- content = `${content.slice(0, fmClose)}\n${summaryYaml}${content.slice(fmClose)}`;
1135
- }
1136
- }
1137
-
1138
- writeFileSync(sprintPath, content, 'utf-8');
1139
- }
1140
-
1141
- // Clear STATE.md current_sprint
1142
- const statePath = join(pmDir, 'STATE.md');
1143
- if (existsSync(statePath)) {
1144
- let stateContent = readFileSync(statePath, 'utf-8');
1145
- stateContent = replaceFrontMatterField(stateContent, 'current_sprint', 'null');
1146
- writeFileSync(statePath, stateContent, 'utf-8');
1147
- }
1148
-
1149
- const updatedSprint = parseSingleSprint(workingDir, sprint.path);
1150
- ctx.broadcastToAll({ type: 'planSprintCompleted', data: updatedSprint });
1151
-
1152
- // Refresh full state
1153
- const updatedState = parsePlanDirectory(workingDir);
1154
- if (updatedState) {
1155
- ctx.broadcastToAll({ type: 'planStateUpdated', data: updatedState });
1156
- }
1157
- }
1158
-
1159
- function handleGetSprintArtifacts(
1160
- ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
1161
- workingDir: string,
1162
- ): void {
1163
- const sprintId = msg.data?.sprintId;
1164
- if (!sprintId) {
1165
- ctx.send(ws, { type: 'planError', data: { error: 'Sprint ID required' } });
1166
- return;
1167
- }
1168
-
1169
- const artifacts = parseSprintArtifacts(workingDir, sprintId);
1170
- if (!artifacts) {
1171
- // Fall back to empty artifacts if sandbox dir doesn't exist yet
1172
- ctx.send(ws, { type: 'planSprintArtifacts', data: { sprintId, progressLog: '', outputFiles: [], reviewResults: [] } });
1173
- return;
1174
- }
1175
-
1176
- ctx.send(ws, { type: 'planSprintArtifacts', data: artifacts });
1177
- }