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,591 +2,38 @@
2
2
  // Licensed under the MIT License. See LICENSE file for details.
3
3
 
4
4
  /**
5
- * PPS Parser — Parses .pm/ (or legacy .plan/) directory files into structured TypeScript objects.
5
+ * PPS Parser — Public API for reading .pm/ (or legacy .plan/) directories.
6
6
  *
7
- * Handles YAML front matter extraction and markdown body parsing.
7
+ * Entity parsing lives in parser-core.ts; migration in parser-migration.ts.
8
8
  */
9
9
 
10
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
+ import { parseBoard, parseIssue, parseMilestone, parseProjectConfig, parseProjectState, parseSprint, parseWorkspace } from './parser-core.js';
13
+ import { isLegacyFormat, migrateToBoards } from './parser-migration.js';
12
14
  import type {
13
- AcceptanceCriterion,
14
15
  Board,
15
16
  BoardArtifacts,
16
- BoardExecutionSummary,
17
17
  BoardFullState,
18
18
  Issue,
19
- IssueSummary,
20
19
  Milestone,
21
- MilestoneEpicSummary,
22
20
  PlanFullState,
23
21
  ProjectConfig,
24
22
  ProjectState,
25
23
  ReviewResult,
26
24
  Sprint,
27
25
  SprintArtifacts,
28
- SprintExecutionSummary,
29
- SprintIssueSummary,
30
- Team,
31
- WorkflowStatus,
32
26
  Workspace,
33
27
  } from './types.js';
34
28
 
35
29
  // ============================================================================
36
- // Front Matter Extraction
30
+ // Directory Resolution
37
31
  // ============================================================================
38
32
 
39
- interface ParsedFile {
40
- frontMatter: Record<string, unknown>;
41
- body: string;
42
- }
43
-
44
- function stripQuotes(v: string): string {
45
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
46
- return v.slice(1, -1);
47
- }
48
- return v;
49
- }
50
-
51
- function parseYamlValue(v: string): unknown {
52
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
53
- return v.slice(1, -1);
54
- }
55
- if (v.startsWith('[') && v.endsWith(']')) {
56
- return v.slice(1, -1).split(',').map(s => stripQuotes(s.trim())).filter(Boolean);
57
- }
58
- if (v === 'true') return true;
59
- if (v === 'false') return false;
60
- if (v === 'null' || v === '~' || v === '') return null;
61
- if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
62
- return v;
63
- }
64
-
65
- /** Consume indented YAML list items starting after the current index. Returns [items, newIndex]. */
66
- function consumeIndentedList(lines: string[], startIdx: number): [string[], number] {
67
- const items: string[] = [];
68
- let i = startIdx;
69
- while (i + 1 < lines.length && /^\s+-\s/.test(lines[i + 1])) {
70
- i++;
71
- const item = lines[i].trim().replace(/^-\s+/, '');
72
- items.push(stripQuotes(item));
73
- }
74
- return [items, i];
75
- }
76
-
77
- function parseFrontMatter(content: string): ParsedFile {
78
- const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
79
- if (!match) {
80
- return { frontMatter: {}, body: content };
81
- }
82
- const frontMatter: Record<string, unknown> = {};
83
- const lines = match[1].split('\n');
84
-
85
- for (let i = 0; i < lines.length; i++) {
86
- const trimmed = lines[i].trim();
87
- if (!trimmed || trimmed.startsWith('#')) continue;
88
- const colonIdx = trimmed.indexOf(':');
89
- if (colonIdx === -1) continue;
90
-
91
- const key = trimmed.slice(0, colonIdx).trim();
92
- const rawValue = trimmed.slice(colonIdx + 1).trim();
93
-
94
- if (!rawValue) {
95
- const [items, newIdx] = consumeIndentedList(lines, i);
96
- i = newIdx;
97
- frontMatter[key] = items.length > 0 ? items : null;
98
- } else {
99
- frontMatter[key] = parseYamlValue(rawValue);
100
- }
101
- }
102
-
103
- return { frontMatter, body: match[2] };
104
- }
105
-
106
- // ============================================================================
107
- // Section Extraction
108
- // ============================================================================
109
-
110
- function extractSections(body: string): Map<string, string> {
111
- const sections = new Map<string, string>();
112
- const lines = body.split('\n');
113
- let currentSection = '';
114
- let currentContent: string[] = [];
115
-
116
- for (const line of lines) {
117
- if (line.startsWith('## ')) {
118
- if (currentSection) {
119
- sections.set(currentSection, currentContent.join('\n').trim());
120
- }
121
- currentSection = line.slice(3).trim();
122
- currentContent = [];
123
- } else {
124
- currentContent.push(line);
125
- }
126
- }
127
- if (currentSection) {
128
- sections.set(currentSection, currentContent.join('\n').trim());
129
- }
130
-
131
- return sections;
132
- }
133
-
134
- function parseCheckboxes(content: string): AcceptanceCriterion[] {
135
- const items: AcceptanceCriterion[] = [];
136
- for (const line of content.split('\n')) {
137
- const match = line.match(/^[-*]\s+\[([ xX])\]\s+(.+)$/);
138
- if (match) {
139
- items.push({ text: match[2].trim(), checked: match[1] !== ' ' });
140
- }
141
- }
142
- return items;
143
- }
144
-
145
- function parseListItems(content: string): string[] {
146
- const items: string[] = [];
147
- for (const line of content.split('\n')) {
148
- const match = line.match(/^[-*]\s+(.+)$/);
149
- if (match) items.push(match[1].trim());
150
- }
151
- return items;
152
- }
153
-
154
- function parseIssueSummaries(content: string): IssueSummary[] {
155
- const summaries: IssueSummary[] = [];
156
- for (const line of content.split('\n')) {
157
- // Match: 1. [IS-003](backlog/IS-003.md) — Title (P1)
158
- const match = line.match(/\d+\.\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*\((\w+)\))?\s*$/);
159
- if (match) {
160
- summaries.push({
161
- id: match[1],
162
- path: match[2],
163
- title: match[3].trim(),
164
- priority: match[4] || '',
165
- });
166
- continue;
167
- }
168
- // Match: - [IS-001](backlog/IS-001.md) — Title
169
- const match2 = line.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*[→→]\s*blocked by\s+\[([^\]]+)\])?\s*$/i);
170
- if (match2) {
171
- summaries.push({
172
- id: match2[1],
173
- path: match2[2],
174
- title: match2[3].trim(),
175
- priority: '',
176
- blockedBy: match2[4] || undefined,
177
- });
178
- }
179
- }
180
- return summaries;
181
- }
182
-
183
- function parseCompletedSummaries(content: string): IssueSummary[] {
184
- const summaries: IssueSummary[] = [];
185
- for (const line of content.split('\n')) {
186
- const match = line.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*[—–-]\s*(.+?)(?:\s*✓)?\s*$/);
187
- if (match) {
188
- summaries.push({
189
- id: match[1],
190
- path: match[2],
191
- title: match[3].trim(),
192
- priority: '',
193
- });
194
- }
195
- }
196
- return summaries;
197
- }
198
-
199
- // ============================================================================
200
- // Entity Parsers
201
- // ============================================================================
202
-
203
- function parseWorkflows(section: string | undefined): WorkflowStatus[] {
204
- if (!section) return [];
205
- const workflows: WorkflowStatus[] = [];
206
- for (const line of section.split('\n')) {
207
- const match = line.match(/\|\s*(\w+)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*\|/);
208
- if (match && match[1] !== 'Status') {
209
- workflows.push({
210
- status: match[1],
211
- category: match[2] as WorkflowStatus['category'],
212
- description: match[3].trim(),
213
- });
214
- }
215
- }
216
- return workflows;
217
- }
218
-
219
- function parseTeams(section: string | undefined): Team[] {
220
- if (!section) return [];
221
- const teams: Team[] = [];
222
- for (const line of section.split('\n')) {
223
- const match = line.match(/^[-*]\s+(\w+)(?:\s*[—–-]\s*(.+))?$/);
224
- if (match) teams.push({ name: match[1], description: match[2]?.trim() });
225
- }
226
- return teams;
227
- }
228
-
229
- function parseProjectConfig(content: string): ProjectConfig {
230
- const { frontMatter, body } = parseFrontMatter(content);
231
- const sections = extractSections(body);
232
-
233
- const idPrefixes: Record<string, string> = {};
234
- const rawPrefixes = frontMatter.id_prefixes;
235
- if (rawPrefixes && typeof rawPrefixes === 'object') {
236
- Object.assign(idPrefixes, rawPrefixes);
237
- }
238
-
239
- return {
240
- name: String(frontMatter.name || ''),
241
- id: String(frontMatter.id || ''),
242
- created: String(frontMatter.created || ''),
243
- status: (frontMatter.status as ProjectConfig['status']) || 'active',
244
- estimation: (frontMatter.estimation as ProjectConfig['estimation']) || 'none',
245
- idPrefixes,
246
- workflows: parseWorkflows(sections.get('Workflows')),
247
- labels: (Array.isArray(frontMatter.labels) ? frontMatter.labels : []) as string[],
248
- teams: parseTeams(sections.get('Teams')),
249
- };
250
- }
251
-
252
- function parseProjectState(content: string): ProjectState {
253
- const { frontMatter, body } = parseFrontMatter(content);
254
- const sections = extractSections(body);
255
-
256
- return {
257
- project: String(frontMatter.project || ''),
258
- currentSprint: (frontMatter.current_sprint as string) || null,
259
- activeMilestone: (frontMatter.active_milestone as string) || null,
260
- paused: frontMatter.paused === true,
261
- lastSession: (frontMatter.last_session as string) || null,
262
- readyToWork: parseIssueSummaries(sections.get('Ready to Work') || ''),
263
- inProgress: parseIssueSummaries(sections.get('In Progress') || ''),
264
- blocked: parseIssueSummaries(sections.get('Blocked') || ''),
265
- recentlyCompleted: parseCompletedSummaries(sections.get('Recently Completed') || ''),
266
- warnings: parseListItems(sections.get('Warnings') || ''),
267
- };
268
- }
269
-
270
- function toStringArray(val: unknown): string[] {
271
- return Array.isArray(val) ? val.map(String) : [];
272
- }
273
-
274
- function optionalString(val: unknown): string | null {
275
- if (val == null) return null;
276
- const s = String(val);
277
- return s === '' ? null : s;
278
- }
279
-
280
- function parseIssue(content: string, filePath: string): Issue {
281
- const { frontMatter: fm, body } = parseFrontMatter(content);
282
- const sections = extractSections(body);
283
-
284
- return {
285
- id: String(fm.id || ''),
286
- title: String(fm.title || ''),
287
- type: (fm.type as Issue['type']) || 'issue',
288
- status: String(fm.status || 'backlog'),
289
- priority: String(fm.priority || 'P2'),
290
- estimate: fm.estimate != null ? fm.estimate as number | string : null,
291
- labels: toStringArray(fm.labels),
292
- epic: optionalString(fm.epic),
293
- sprint: optionalString(fm.sprint),
294
- milestone: optionalString(fm.milestone),
295
- assigned: optionalString(fm.assigned),
296
- created: String(fm.created || ''),
297
- updated: optionalString(fm.updated),
298
- due: optionalString(fm.due),
299
- blockedBy: toStringArray(fm.blocked_by),
300
- blocks: toStringArray(fm.blocks),
301
- relatesTo: toStringArray(fm.relates_to),
302
- children: toStringArray(fm.children),
303
- progress: optionalString(fm.progress),
304
- description: sections.get('Description') || '',
305
- acceptanceCriteria: parseCheckboxes(sections.get('Acceptance Criteria') || ''),
306
- technicalNotes: sections.get('Technical Notes') || null,
307
- filesToModify: parseListItems(sections.get('Files to Modify') || ''),
308
- activity: parseListItems(sections.get('Activity') || ''),
309
- reviewGate: (['none', 'auto', 'required'].includes(String(fm.review_gate)) ? String(fm.review_gate) : 'auto') as Issue['reviewGate'],
310
- outputFile: optionalString(fm.output_file),
311
- body,
312
- path: filePath,
313
- };
314
- }
315
-
316
- function parseSprintIssues(section: string | undefined): SprintIssueSummary[] {
317
- if (!section) return [];
318
- const issues: SprintIssueSummary[] = [];
319
- for (const line of section.split('\n')) {
320
- const match = line.match(/\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|/);
321
- if (match) {
322
- issues.push({
323
- id: match[1],
324
- path: match[2],
325
- title: match[3].trim(),
326
- points: /^\d+$/.test(match[4]) ? Number(match[4]) : match[4],
327
- status: match[5],
328
- });
329
- }
330
- }
331
- return issues;
332
- }
333
-
334
- function optionalNumber(val: unknown): number | null {
335
- return val != null ? Number(val) : null;
336
- }
337
-
338
- function parseSprint(content: string, filePath: string): Sprint {
339
- const { frontMatter: fm, body } = parseFrontMatter(content);
340
- const sections = extractSections(body);
341
-
342
- // Table-based parsing (markdown links in table rows)
343
- let issues = parseSprintIssues(sections.get('Issues'));
344
-
345
- // Fallback: front matter issues array (e.g., ["backlog/IS-001.md", ...])
346
- if (issues.length === 0 && Array.isArray(fm.issues)) {
347
- issues = (fm.issues as string[]).map(path => {
348
- const id = path.replace(/^backlog\//, '').replace(/\.md$/, '');
349
- return { id, path, title: '', points: null, status: '' };
350
- });
351
- }
352
-
353
- // Parse execution_summary if present (JSON object in front matter)
354
- let executionSummary: SprintExecutionSummary | null = null;
355
- if (fm.execution_summary && typeof fm.execution_summary === 'object') {
356
- const es = fm.execution_summary as Record<string, unknown>;
357
- executionSummary = {
358
- totalIssues: Number(es.total_issues ?? 0),
359
- completedIssues: Number(es.completed_issues ?? 0),
360
- failedIssues: Number(es.failed_issues ?? 0),
361
- totalDuration: Number(es.total_duration ?? 0),
362
- waves: Number(es.waves ?? 0),
363
- };
364
- }
365
-
366
- return {
367
- id: String(fm.id || ''),
368
- title: String(fm.title || ''),
369
- status: (fm.status as Sprint['status']) || 'planned',
370
- start: String(fm.start || fm.start_date || ''),
371
- end: String(fm.end || fm.end_date || ''),
372
- goal: String(fm.goal || sections.get('Goal') || sections.get('Sprint Goal') || ''),
373
- capacity: optionalNumber(fm.capacity),
374
- committed: optionalNumber(fm.committed),
375
- completed: optionalNumber(fm.completed),
376
- issues,
377
- path: filePath,
378
- completedAt: optionalString(fm.completed_at),
379
- executionSummary,
380
- };
381
- }
382
-
383
- function parseMilestone(content: string, filePath: string): Milestone {
384
- const { frontMatter, body } = parseFrontMatter(content);
385
- const sections = extractSections(body);
386
-
387
- const epics: MilestoneEpicSummary[] = [];
388
- const epicSection = sections.get('Epics');
389
- if (epicSection) {
390
- for (const line of epicSection.split('\n')) {
391
- const match = line.match(/\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|\s*(\S+)\s*\|/);
392
- if (match) {
393
- epics.push({
394
- id: match[1],
395
- path: match[2],
396
- title: match[3].trim(),
397
- progress: match[4],
398
- });
399
- }
400
- }
401
- }
402
-
403
- return {
404
- id: String(frontMatter.id || ''),
405
- title: String(frontMatter.title || ''),
406
- status: (frontMatter.status as Milestone['status']) || 'planned',
407
- targetDate: (frontMatter.target_date as string) || null,
408
- progress: (frontMatter.progress as string) || null,
409
- definition: sections.get('Definition of Done') || '',
410
- epics,
411
- path: filePath,
412
- };
413
- }
414
-
415
- // ============================================================================
416
- // Board Parser
417
- // ============================================================================
418
-
419
- function parseBoard(content: string, filePath: string): Board {
420
- const { frontMatter: fm, body } = parseFrontMatter(content);
421
- const sections = extractSections(body);
422
-
423
- let executionSummary: BoardExecutionSummary | null = null;
424
- if (fm.execution_summary && typeof fm.execution_summary === 'object') {
425
- const es = fm.execution_summary as Record<string, unknown>;
426
- executionSummary = {
427
- totalIssues: Number(es.total_issues ?? 0),
428
- completedIssues: Number(es.completed_issues ?? 0),
429
- failedIssues: Number(es.failed_issues ?? 0),
430
- totalDuration: Number(es.total_duration ?? 0),
431
- waves: Number(es.waves ?? 0),
432
- };
433
- }
434
-
435
- return {
436
- id: String(fm.id || ''),
437
- title: String(fm.title || ''),
438
- status: (fm.status as Board['status']) || 'draft',
439
- created: String(fm.created || ''),
440
- completedAt: optionalString(fm.completed_at),
441
- goal: String(fm.goal || sections.get('Goal') || ''),
442
- executionSummary,
443
- path: filePath,
444
- };
445
- }
446
-
447
- function parseWorkspace(content: string): Workspace {
448
- try {
449
- const parsed = JSON.parse(content) as Record<string, unknown>;
450
- return {
451
- activeBoardId: typeof parsed.activeBoardId === 'string' ? parsed.activeBoardId : null,
452
- boardOrder: Array.isArray(parsed.boardOrder) ? parsed.boardOrder.map(String) : [],
453
- };
454
- } catch {
455
- return { activeBoardId: null, boardOrder: [] };
456
- }
457
- }
458
-
459
- /** Check whether a .pm/ directory uses the board-centric format (has boards/ subdirectory). */
460
33
  export function isBoardCentricFormat(pmDir: string): boolean {
461
34
  return existsSync(join(pmDir, 'boards'));
462
35
  }
463
36
 
464
- /** Check whether a .pm/ directory uses the legacy flat format (has backlog/ at root, no boards/). */
465
- function isLegacyFormat(pmDir: string): boolean {
466
- return existsSync(join(pmDir, 'backlog')) && !existsSync(join(pmDir, 'boards'));
467
- }
468
-
469
- // ============================================================================
470
- // Legacy → Board Migration
471
- // ============================================================================
472
-
473
- /** Move all files from a legacy directory into a board subdirectory and remove the source. */
474
- function moveLegacyDir(srcDir: string, destDir: string): void {
475
- if (!existsSync(srcDir)) return;
476
- for (const file of readdirSync(srcDir)) {
477
- renameSync(join(srcDir, file), join(destDir, file));
478
- }
479
- rmSync(srcDir, { recursive: true });
480
- }
481
-
482
- /** Move a single file if it exists. */
483
- function moveLegacyFile(src: string, dest: string): void {
484
- if (existsSync(src)) renameSync(src, dest);
485
- }
486
-
487
- /** Copy review files from sprint sandbox directories into the board reviews dir. */
488
- function copySprintReviews(sprintsDir: string, boardReviewsDir: string): void {
489
- for (const entry of readdirSync(sprintsDir)) {
490
- if (entry.endsWith('.md')) continue;
491
- const reviewsDir = join(sprintsDir, entry, 'reviews');
492
- if (!existsSync(reviewsDir)) continue;
493
- for (const reviewFile of readdirSync(reviewsDir)) {
494
- cpSync(join(reviewsDir, reviewFile), join(boardReviewsDir, reviewFile));
495
- }
496
- }
497
- }
498
-
499
- /** Find and return the goal from the active sprint .md file. */
500
- function extractActiveSprintGoal(sprintsDir: string): string {
501
- for (const entry of readdirSync(sprintsDir).filter(e => e.endsWith('.md'))) {
502
- const content = readFileIfExists(join(sprintsDir, entry));
503
- if (!content) continue;
504
- const fm = parseFrontMatter(content).frontMatter;
505
- if (fm.status === 'active') return String(fm.goal || '');
506
- }
507
- return '';
508
- }
509
-
510
- /** Migrate sprint reviews and extract the active sprint's goal. */
511
- function migrateLegacySprints(sprintsDir: string, boardReviewsDir: string): string {
512
- if (!existsSync(sprintsDir)) return '';
513
- copySprintReviews(sprintsDir, boardReviewsDir);
514
- const goal = extractActiveSprintGoal(sprintsDir);
515
- rmSync(sprintsDir, { recursive: true });
516
- return goal;
517
- }
518
-
519
- /** Clean up migrated issues: remove sprint fields, detect active issues. */
520
- function cleanupMigratedIssues(boardBacklogDir: string): boolean {
521
- if (!existsSync(boardBacklogDir)) return false;
522
- let hasActive = false;
523
-
524
- for (const file of readdirSync(boardBacklogDir).filter(f => f.endsWith('.md'))) {
525
- const content = readFileIfExists(join(boardBacklogDir, file));
526
- if (!content) continue;
527
- if (content.match(/^status:\s*(in_progress|in_review|todo)/m)) hasActive = true;
528
- if (content.match(/^sprint:\s*.+$/m)) {
529
- writeFileSync(join(boardBacklogDir, file), content.replace(/^sprint:\s*.+\n?/m, ''), 'utf-8');
530
- }
531
- }
532
- return hasActive;
533
- }
534
-
535
- /** Write the board metadata files (board.md, workspace.json, STATE.md, progress.md). */
536
- function writeBoardMetadata(pmDir: string, boardDir: string, boardId: string, sprintGoal: string, hasActive: boolean): void {
537
- const today = new Date().toISOString().slice(0, 10);
538
- const boardMd = [
539
- '---', `id: ${boardId}`, 'title: "Board 1"',
540
- `status: ${hasActive ? 'active' : 'draft'}`, `created: "${today}"`,
541
- 'completed_at: null', `goal: "${sprintGoal.replace(/"/g, '\\"')}"`,
542
- '---', '', '# Board 1', '',
543
- sprintGoal ? `## Goal\n${sprintGoal}\n` : '',
544
- ].join('\n');
545
- writeFileSync(join(boardDir, 'board.md'), boardMd, 'utf-8');
546
-
547
- const workspace: Workspace = { activeBoardId: boardId, boardOrder: [boardId] };
548
- writeFileSync(join(pmDir, 'workspace.json'), JSON.stringify(workspace, null, 2), 'utf-8');
549
-
550
- if (!existsSync(join(boardDir, 'STATE.md'))) {
551
- writeFileSync(join(boardDir, 'STATE.md'), [
552
- '---', 'project: ../../project.md', 'board: board.md', 'paused: false', '---', '',
553
- '# Board State', '', '## Ready to Work', '', '## In Progress', '',
554
- '## Blocked', '', '## Recently Completed', '', '## Warnings', '',
555
- ].join('\n'), 'utf-8');
556
- }
557
- if (!existsSync(join(boardDir, 'progress.md'))) {
558
- writeFileSync(join(boardDir, 'progress.md'), '# Board Progress\n', 'utf-8');
559
- }
560
- }
561
-
562
- /**
563
- * Migrate a legacy flat .pm/ directory to board-centric format.
564
- * Creates BOARD-001 from the existing backlog, state, outputs, and reviews.
565
- */
566
- function migrateToBoards(pmDir: string): void {
567
- const boardId = 'BOARD-001';
568
- const boardDir = join(pmDir, 'boards', boardId);
569
-
570
- mkdirSync(boardDir, { recursive: true });
571
- mkdirSync(join(boardDir, 'backlog'), { recursive: true });
572
- mkdirSync(join(boardDir, 'out'), { recursive: true });
573
- mkdirSync(join(boardDir, 'reviews'), { recursive: true });
574
-
575
- moveLegacyDir(join(pmDir, 'backlog'), join(boardDir, 'backlog'));
576
- moveLegacyFile(join(pmDir, 'STATE.md'), join(boardDir, 'STATE.md'));
577
- moveLegacyDir(join(pmDir, 'out'), join(boardDir, 'out'));
578
- moveLegacyFile(join(pmDir, 'progress.md'), join(boardDir, 'progress.md'));
579
-
580
- const sprintGoal = migrateLegacySprints(join(pmDir, 'sprints'), join(boardDir, 'reviews'));
581
- const hasActive = cleanupMigratedIssues(join(boardDir, 'backlog'));
582
- writeBoardMetadata(pmDir, boardDir, boardId, sprintGoal, hasActive);
583
- }
584
-
585
- // ============================================================================
586
- // Directory Parser
587
- // ============================================================================
588
-
589
- /** Resolve the PM directory — prefers .mstro/pm/, falls back to legacy .pm/ and .plan/ */
590
37
  export function resolvePmDir(workingDir: string): string | null {
591
38
  const mstroPmDir = join(workingDir, '.mstro', 'pm');
592
39
  if (existsSync(mstroPmDir)) return mstroPmDir;
@@ -597,7 +44,6 @@ export function resolvePmDir(workingDir: string): string | null {
597
44
  return null;
598
45
  }
599
46
 
600
- /** Default PM directory path for new projects */
601
47
  export function defaultPmDir(workingDir: string): string {
602
48
  return join(workingDir, '.mstro', 'pm');
603
49
  }
@@ -606,6 +52,10 @@ export function planDirExists(workingDir: string): boolean {
606
52
  return resolvePmDir(workingDir) !== null;
607
53
  }
608
54
 
55
+ // ============================================================================
56
+ // File Utilities
57
+ // ============================================================================
58
+
609
59
  function readFileIfExists(path: string): string | null {
610
60
  try {
611
61
  if (existsSync(path)) return readFileSync(path, 'utf-8');
@@ -618,13 +68,30 @@ function readMdFilesInDir(dirPath: string): Array<{ name: string; content: strin
618
68
  try {
619
69
  return readdirSync(dirPath)
620
70
  .filter(f => f.endsWith('.md'))
621
- .map(name => {
622
- const content = readFileSync(join(dirPath, name), 'utf-8');
623
- return { name, content };
624
- });
71
+ .map(name => ({ name, content: readFileSync(join(dirPath, name), 'utf-8') }));
72
+ } catch { return []; }
73
+ }
74
+
75
+ function listDirFiles(dirPath: string, ext: string): string[] {
76
+ if (!existsSync(dirPath)) return [];
77
+ try {
78
+ return readdirSync(dirPath).filter(f => f.endsWith(ext));
625
79
  } catch { return []; }
626
80
  }
627
81
 
82
+ function readReviewResults(reviewsDir: string): ReviewResult[] {
83
+ const results: ReviewResult[] = [];
84
+ for (const f of listDirFiles(reviewsDir, '.json')) {
85
+ const content = readFileIfExists(join(reviewsDir, f));
86
+ if (content) results.push(JSON.parse(content) as ReviewResult);
87
+ }
88
+ return results;
89
+ }
90
+
91
+ // ============================================================================
92
+ // Defaults
93
+ // ============================================================================
94
+
628
95
  const defaultProject: ProjectConfig = {
629
96
  name: '', id: '', created: '', status: 'active', estimation: 'none',
630
97
  idPrefixes: {}, workflows: [], labels: [], teams: [],
@@ -636,7 +103,10 @@ const defaultState: ProjectState = {
636
103
  recentlyCompleted: [], warnings: [],
637
104
  };
638
105
 
639
- /** Parse a single board's full state (board.md + STATE.md + backlog/). */
106
+ // ============================================================================
107
+ // Board & Plan Directory Parsing
108
+ // ============================================================================
109
+
640
110
  export function parseBoardDirectory(pmDir: string, boardId: string): BoardFullState | null {
641
111
  const boardDir = join(pmDir, 'boards', boardId);
642
112
  if (!existsSync(boardDir)) return null;
@@ -652,7 +122,6 @@ export function parseBoardDirectory(pmDir: string, boardId: string): BoardFullSt
652
122
  const boardPrefix = `boards/${boardId}/`;
653
123
  const issues = issueFiles.map(f => {
654
124
  const issue = parseIssue(f.content, `${boardPrefix}backlog/${f.name}`);
655
- // Normalize blocked_by/blocks to full board-relative paths so dependency resolver matches
656
125
  issue.blockedBy = issue.blockedBy.map(bp => bp.startsWith('boards/') ? bp : `${boardPrefix}${bp}`);
657
126
  issue.blocks = issue.blocks.map(bp => bp.startsWith('boards/') ? bp : `${boardPrefix}${bp}`);
658
127
  if (issue.epic && !issue.epic.startsWith('boards/')) issue.epic = `${boardPrefix}${issue.epic}`;
@@ -662,7 +131,6 @@ export function parseBoardDirectory(pmDir: string, boardId: string): BoardFullSt
662
131
  return { board, state, issues };
663
132
  }
664
133
 
665
- /** Parse all boards from the boards/ directory and resolve the active board. */
666
134
  function parseBoardCentricState(planDir: string): { boards: Board[]; workspace: Workspace; activeBoard: BoardFullState | null } {
667
135
  const workspaceContent = readFileIfExists(join(planDir, 'workspace.json'));
668
136
  const workspace = workspaceContent ? parseWorkspace(workspaceContent) : { activeBoardId: null, boardOrder: [] };
@@ -710,11 +178,14 @@ export function parsePlanDirectory(workingDir: string): PlanFullState | null {
710
178
  };
711
179
  }
712
180
 
181
+ // ============================================================================
182
+ // Single Entity Parsers
183
+ // ============================================================================
184
+
713
185
  export function parseSingleIssue(workingDir: string, issuePath: string): Issue | null {
714
186
  const pmDir = resolvePmDir(workingDir);
715
187
  if (!pmDir) return null;
716
- const fullPath = join(pmDir, issuePath);
717
- const content = readFileIfExists(fullPath);
188
+ const content = readFileIfExists(join(pmDir, issuePath));
718
189
  if (!content) return null;
719
190
  return parseIssue(content, issuePath);
720
191
  }
@@ -722,8 +193,7 @@ export function parseSingleIssue(workingDir: string, issuePath: string): Issue |
722
193
  export function parseSingleSprint(workingDir: string, sprintPath: string): Sprint | null {
723
194
  const pmDir = resolvePmDir(workingDir);
724
195
  if (!pmDir) return null;
725
- const fullPath = join(pmDir, sprintPath);
726
- const content = readFileIfExists(fullPath);
196
+ const content = readFileIfExists(join(pmDir, sprintPath));
727
197
  if (!content) return null;
728
198
  return parseSprint(content, sprintPath);
729
199
  }
@@ -731,13 +201,15 @@ export function parseSingleSprint(workingDir: string, sprintPath: string): Sprin
731
201
  export function parseSingleMilestone(workingDir: string, milestonePath: string): Milestone | null {
732
202
  const pmDir = resolvePmDir(workingDir);
733
203
  if (!pmDir) return null;
734
- const fullPath = join(pmDir, milestonePath);
735
- const content = readFileIfExists(fullPath);
204
+ const content = readFileIfExists(join(pmDir, milestonePath));
736
205
  if (!content) return null;
737
206
  return parseMilestone(content, milestonePath);
738
207
  }
739
208
 
740
- /** Compute the next available ID for a given prefix (e.g., "IS" → "IS-004") */
209
+ // ============================================================================
210
+ // ID Generation
211
+ // ============================================================================
212
+
741
213
  export function getNextId(issues: Issue[], prefix: string): string {
742
214
  const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
743
215
  const pattern = new RegExp(`^${escaped}-(\\d+)$`);
@@ -752,7 +224,6 @@ export function getNextId(issues: Issue[], prefix: string): string {
752
224
  return `${prefix}-${String(max + 1).padStart(3, '0')}`;
753
225
  }
754
226
 
755
- /** Compute the next available board ID (e.g., "BOARD-003") */
756
227
  export function getNextBoardId(boards: Board[]): string {
757
228
  let max = 0;
758
229
  for (const board of boards) {
@@ -765,7 +236,6 @@ export function getNextBoardId(boards: Board[]): string {
765
236
  return `BOARD-${String(max + 1).padStart(3, '0')}`;
766
237
  }
767
238
 
768
- /** Compute the next available board number for display title (e.g., "Board 3") */
769
239
  export function getNextBoardNumber(boards: Board[]): number {
770
240
  let max = 0;
771
241
  for (const board of boards) {
@@ -778,7 +248,6 @@ export function getNextBoardNumber(boards: Board[]): number {
778
248
  return max + 1;
779
249
  }
780
250
 
781
- /** Parse board artifacts from boards/BOARD-N/ directory. */
782
251
  export function parseBoardArtifacts(workingDir: string, boardId: string): BoardArtifacts | null {
783
252
  const pmDir = resolvePmDir(workingDir);
784
253
  if (!pmDir) return null;
@@ -787,29 +256,11 @@ export function parseBoardArtifacts(workingDir: string, boardId: string): BoardA
787
256
  if (!existsSync(boardDir)) return null;
788
257
 
789
258
  const progressLog = readFileIfExists(join(boardDir, 'progress.md')) ?? '';
259
+ const outputFiles = listDirFiles(join(boardDir, 'out'), '.md');
260
+ const reviewResults = readReviewResults(join(boardDir, 'reviews'));
261
+ const executionLogs = listDirFiles(join(boardDir, 'logs'), '.log').sort();
790
262
 
791
- const outDir = join(boardDir, 'out');
792
- let outputFiles: string[] = [];
793
- if (existsSync(outDir)) {
794
- try {
795
- outputFiles = readdirSync(outDir).filter(f => f.endsWith('.md'));
796
- } catch { /* skip */ }
797
- }
798
-
799
- const reviewsDir = join(boardDir, 'reviews');
800
- const reviewResults: ReviewResult[] = [];
801
- if (existsSync(reviewsDir)) {
802
- try {
803
- for (const f of readdirSync(reviewsDir).filter(f => f.endsWith('.json'))) {
804
- const content = readFileIfExists(join(reviewsDir, f));
805
- if (content) {
806
- reviewResults.push(JSON.parse(content) as ReviewResult);
807
- }
808
- }
809
- } catch { /* skip */ }
810
- }
811
-
812
- return { boardId, progressLog, outputFiles, reviewResults };
263
+ return { boardId, progressLog, outputFiles, reviewResults, executionLogs };
813
264
  }
814
265
 
815
266
  /** @deprecated Use getNextBoardId — kept for migration compatibility */
@@ -834,27 +285,8 @@ export function parseSprintArtifacts(workingDir: string, sprintId: string): Spri
834
285
  if (!existsSync(sandboxDir)) return null;
835
286
 
836
287
  const progressLog = readFileIfExists(join(sandboxDir, 'progress.md')) ?? '';
837
-
838
- const outDir = join(sandboxDir, 'out');
839
- let outputFiles: string[] = [];
840
- if (existsSync(outDir)) {
841
- try {
842
- outputFiles = readdirSync(outDir).filter(f => f.endsWith('.md'));
843
- } catch { /* skip */ }
844
- }
845
-
846
- const reviewsDir = join(sandboxDir, 'reviews');
847
- const reviewResults: ReviewResult[] = [];
848
- if (existsSync(reviewsDir)) {
849
- try {
850
- for (const f of readdirSync(reviewsDir).filter(f => f.endsWith('.json'))) {
851
- const content = readFileIfExists(join(reviewsDir, f));
852
- if (content) {
853
- reviewResults.push(JSON.parse(content) as ReviewResult);
854
- }
855
- }
856
- } catch { /* skip */ }
857
- }
288
+ const outputFiles = listDirFiles(join(sandboxDir, 'out'), '.md');
289
+ const reviewResults = readReviewResults(join(sandboxDir, 'reviews'));
858
290
 
859
291
  return { sprintId, progressLog, outputFiles, reviewResults };
860
292
  }