@lbruton/spec-workflow-mcp 2.2.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 (407) hide show
  1. package/CHANGELOG.md +955 -0
  2. package/LICENSE +674 -0
  3. package/README.ar.md +314 -0
  4. package/README.de.md +314 -0
  5. package/README.es.md +314 -0
  6. package/README.fr.md +314 -0
  7. package/README.it.md +314 -0
  8. package/README.ja.md +316 -0
  9. package/README.ko.md +314 -0
  10. package/README.md +373 -0
  11. package/README.pt.md +314 -0
  12. package/README.ru.md +314 -0
  13. package/README.zh.md +314 -0
  14. package/dist/__tests__/config.test.d.ts +2 -0
  15. package/dist/__tests__/config.test.d.ts.map +1 -0
  16. package/dist/__tests__/config.test.js +264 -0
  17. package/dist/__tests__/config.test.js.map +1 -0
  18. package/dist/__tests__/index-args.test.d.ts +2 -0
  19. package/dist/__tests__/index-args.test.d.ts.map +1 -0
  20. package/dist/__tests__/index-args.test.js +43 -0
  21. package/dist/__tests__/index-args.test.js.map +1 -0
  22. package/dist/__tests__/index-entrypoint.test.d.ts +2 -0
  23. package/dist/__tests__/index-entrypoint.test.d.ts.map +1 -0
  24. package/dist/__tests__/index-entrypoint.test.js +23 -0
  25. package/dist/__tests__/index-entrypoint.test.js.map +1 -0
  26. package/dist/config.d.ts +26 -0
  27. package/dist/config.d.ts.map +1 -0
  28. package/dist/config.js +188 -0
  29. package/dist/config.js.map +1 -0
  30. package/dist/core/__tests__/git-utils.test.d.ts +2 -0
  31. package/dist/core/__tests__/git-utils.test.d.ts.map +1 -0
  32. package/dist/core/__tests__/git-utils.test.js +179 -0
  33. package/dist/core/__tests__/git-utils.test.js.map +1 -0
  34. package/dist/core/__tests__/mdx-validator.test.d.ts +2 -0
  35. package/dist/core/__tests__/mdx-validator.test.d.ts.map +1 -0
  36. package/dist/core/__tests__/mdx-validator.test.js +42 -0
  37. package/dist/core/__tests__/mdx-validator.test.js.map +1 -0
  38. package/dist/core/__tests__/path-utils.test.d.ts +2 -0
  39. package/dist/core/__tests__/path-utils.test.d.ts.map +1 -0
  40. package/dist/core/__tests__/path-utils.test.js +344 -0
  41. package/dist/core/__tests__/path-utils.test.js.map +1 -0
  42. package/dist/core/__tests__/project-registry.test.d.ts +2 -0
  43. package/dist/core/__tests__/project-registry.test.d.ts.map +1 -0
  44. package/dist/core/__tests__/project-registry.test.js +62 -0
  45. package/dist/core/__tests__/project-registry.test.js.map +1 -0
  46. package/dist/core/__tests__/security-utils.test.d.ts +2 -0
  47. package/dist/core/__tests__/security-utils.test.d.ts.map +1 -0
  48. package/dist/core/__tests__/security-utils.test.js +643 -0
  49. package/dist/core/__tests__/security-utils.test.js.map +1 -0
  50. package/dist/core/__tests__/task-validator.test.d.ts +2 -0
  51. package/dist/core/__tests__/task-validator.test.d.ts.map +1 -0
  52. package/dist/core/__tests__/task-validator.test.js +237 -0
  53. package/dist/core/__tests__/task-validator.test.js.map +1 -0
  54. package/dist/core/archive-service.d.ts +10 -0
  55. package/dist/core/archive-service.d.ts.map +1 -0
  56. package/dist/core/archive-service.js +99 -0
  57. package/dist/core/archive-service.js.map +1 -0
  58. package/dist/core/dashboard-session.d.ts +49 -0
  59. package/dist/core/dashboard-session.d.ts.map +1 -0
  60. package/dist/core/dashboard-session.js +132 -0
  61. package/dist/core/dashboard-session.js.map +1 -0
  62. package/dist/core/git-utils.d.ts +25 -0
  63. package/dist/core/git-utils.d.ts.map +1 -0
  64. package/dist/core/git-utils.js +87 -0
  65. package/dist/core/git-utils.js.map +1 -0
  66. package/dist/core/global-dir.d.ts +44 -0
  67. package/dist/core/global-dir.d.ts.map +1 -0
  68. package/dist/core/global-dir.js +74 -0
  69. package/dist/core/global-dir.js.map +1 -0
  70. package/dist/core/implementation-log-migrator.d.ts +41 -0
  71. package/dist/core/implementation-log-migrator.d.ts.map +1 -0
  72. package/dist/core/implementation-log-migrator.js +258 -0
  73. package/dist/core/implementation-log-migrator.js.map +1 -0
  74. package/dist/core/mdx-validator.d.ts +14 -0
  75. package/dist/core/mdx-validator.d.ts.map +1 -0
  76. package/dist/core/mdx-validator.js +34 -0
  77. package/dist/core/mdx-validator.js.map +1 -0
  78. package/dist/core/parser.d.ts +11 -0
  79. package/dist/core/parser.d.ts.map +1 -0
  80. package/dist/core/parser.js +126 -0
  81. package/dist/core/parser.js.map +1 -0
  82. package/dist/core/path-utils.d.ts +63 -0
  83. package/dist/core/path-utils.d.ts.map +1 -0
  84. package/dist/core/path-utils.js +288 -0
  85. package/dist/core/path-utils.js.map +1 -0
  86. package/dist/core/project-registry.d.ts +94 -0
  87. package/dist/core/project-registry.d.ts.map +1 -0
  88. package/dist/core/project-registry.js +297 -0
  89. package/dist/core/project-registry.js.map +1 -0
  90. package/dist/core/security-utils.d.ts +97 -0
  91. package/dist/core/security-utils.d.ts.map +1 -0
  92. package/dist/core/security-utils.js +264 -0
  93. package/dist/core/security-utils.js.map +1 -0
  94. package/dist/core/task-parser.d.ts +63 -0
  95. package/dist/core/task-parser.d.ts.map +1 -0
  96. package/dist/core/task-parser.js +332 -0
  97. package/dist/core/task-parser.js.map +1 -0
  98. package/dist/core/task-validator.d.ts +35 -0
  99. package/dist/core/task-validator.d.ts.map +1 -0
  100. package/dist/core/task-validator.js +236 -0
  101. package/dist/core/task-validator.js.map +1 -0
  102. package/dist/core/workspace-initializer.d.ts +16 -0
  103. package/dist/core/workspace-initializer.d.ts.map +1 -0
  104. package/dist/core/workspace-initializer.js +165 -0
  105. package/dist/core/workspace-initializer.js.map +1 -0
  106. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.d.ts +2 -0
  107. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.d.ts.map +1 -0
  108. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.js +69 -0
  109. package/dist/dashboard/__tests__/approval-storage-path-resolution.test.js.map +1 -0
  110. package/dist/dashboard/__tests__/multi-server-approvals-content.test.d.ts +2 -0
  111. package/dist/dashboard/__tests__/multi-server-approvals-content.test.d.ts.map +1 -0
  112. package/dist/dashboard/__tests__/multi-server-approvals-content.test.js +116 -0
  113. package/dist/dashboard/__tests__/multi-server-approvals-content.test.js.map +1 -0
  114. package/dist/dashboard/__tests__/watcher-error-handling.test.d.ts +2 -0
  115. package/dist/dashboard/__tests__/watcher-error-handling.test.d.ts.map +1 -0
  116. package/dist/dashboard/__tests__/watcher-error-handling.test.js +118 -0
  117. package/dist/dashboard/__tests__/watcher-error-handling.test.js.map +1 -0
  118. package/dist/dashboard/approval-storage.d.ts +139 -0
  119. package/dist/dashboard/approval-storage.d.ts.map +1 -0
  120. package/dist/dashboard/approval-storage.js +586 -0
  121. package/dist/dashboard/approval-storage.js.map +1 -0
  122. package/dist/dashboard/execution-history-manager.d.ts +52 -0
  123. package/dist/dashboard/execution-history-manager.d.ts.map +1 -0
  124. package/dist/dashboard/execution-history-manager.js +161 -0
  125. package/dist/dashboard/execution-history-manager.js.map +1 -0
  126. package/dist/dashboard/implementation-log-manager.d.ts +97 -0
  127. package/dist/dashboard/implementation-log-manager.d.ts.map +1 -0
  128. package/dist/dashboard/implementation-log-manager.js +586 -0
  129. package/dist/dashboard/implementation-log-manager.js.map +1 -0
  130. package/dist/dashboard/job-scheduler.d.ts +91 -0
  131. package/dist/dashboard/job-scheduler.d.ts.map +1 -0
  132. package/dist/dashboard/job-scheduler.js +321 -0
  133. package/dist/dashboard/job-scheduler.js.map +1 -0
  134. package/dist/dashboard/multi-server.d.ts +42 -0
  135. package/dist/dashboard/multi-server.d.ts.map +1 -0
  136. package/dist/dashboard/multi-server.js +1313 -0
  137. package/dist/dashboard/multi-server.js.map +1 -0
  138. package/dist/dashboard/parser.d.ts +18 -0
  139. package/dist/dashboard/parser.d.ts.map +1 -0
  140. package/dist/dashboard/parser.js +243 -0
  141. package/dist/dashboard/parser.js.map +1 -0
  142. package/dist/dashboard/project-manager.d.ts +82 -0
  143. package/dist/dashboard/project-manager.d.ts.map +1 -0
  144. package/dist/dashboard/project-manager.js +257 -0
  145. package/dist/dashboard/project-manager.js.map +1 -0
  146. package/dist/dashboard/public/assets/Inter-Bold-CD3Pr7BX.woff2 +0 -0
  147. package/dist/dashboard/public/assets/Inter-Medium-B_8v_WHh.woff2 +0 -0
  148. package/dist/dashboard/public/assets/Inter-Regular-DRVdRqcI.woff2 +0 -0
  149. package/dist/dashboard/public/assets/Inter-SemiBold-CtskMddL.woff2 +0 -0
  150. package/dist/dashboard/public/assets/JetBrainsMono-Bold-D4WEaHbo.woff2 +0 -0
  151. package/dist/dashboard/public/assets/JetBrainsMono-Medium-3S3k2nMz.woff2 +0 -0
  152. package/dist/dashboard/public/assets/JetBrainsMono-Regular-BQaDgvhP.woff2 +0 -0
  153. package/dist/dashboard/public/assets/Tableau10-B-NsZVaP.js +1 -0
  154. package/dist/dashboard/public/assets/apl-B4CMkyY2.js +1 -0
  155. package/dist/dashboard/public/assets/arc-C8LPXB-J.js +1 -0
  156. package/dist/dashboard/public/assets/array-BKyUJesY.js +1 -0
  157. package/dist/dashboard/public/assets/asciiarmor-Df11BRmG.js +1 -0
  158. package/dist/dashboard/public/assets/asn1-EdZsLKOL.js +1 -0
  159. package/dist/dashboard/public/assets/asterisk-B-8jnY81.js +1 -0
  160. package/dist/dashboard/public/assets/blockDiagram-c4efeb88-RidjsOEy.js +118 -0
  161. package/dist/dashboard/public/assets/brainfuck-C4LP7Hcl.js +1 -0
  162. package/dist/dashboard/public/assets/c4Diagram-c83219d4-CAH3hSpm.js +10 -0
  163. package/dist/dashboard/public/assets/channel-CmDIZRCD.js +1 -0
  164. package/dist/dashboard/public/assets/classDiagram-beda092f-Bo46Efmw.js +2 -0
  165. package/dist/dashboard/public/assets/classDiagram-v2-2358418a-Be57sb3z.js +2 -0
  166. package/dist/dashboard/public/assets/clike-B9uivgTg.js +1 -0
  167. package/dist/dashboard/public/assets/clojure-BMjYHr_A.js +1 -0
  168. package/dist/dashboard/public/assets/clone-BiekPeZp.js +1 -0
  169. package/dist/dashboard/public/assets/cmake-BQqOBYOt.js +1 -0
  170. package/dist/dashboard/public/assets/cobol-CWcv1MsR.js +1 -0
  171. package/dist/dashboard/public/assets/coffeescript-S37ZYGWr.js +1 -0
  172. package/dist/dashboard/public/assets/commonlisp-DBKNyK5s.js +1 -0
  173. package/dist/dashboard/public/assets/createText-1719965b-YurEYFNx.js +7 -0
  174. package/dist/dashboard/public/assets/crystal-SjHAIU92.js +1 -0
  175. package/dist/dashboard/public/assets/css-BnMrqG3P.js +1 -0
  176. package/dist/dashboard/public/assets/cypher-C_CwsFkJ.js +1 -0
  177. package/dist/dashboard/public/assets/d-pRatUO7H.js +1 -0
  178. package/dist/dashboard/public/assets/diff-DbItnlRl.js +1 -0
  179. package/dist/dashboard/public/assets/dockerfile-BKs6k2Af.js +1 -0
  180. package/dist/dashboard/public/assets/dtd-DF_7sFjM.js +1 -0
  181. package/dist/dashboard/public/assets/dylan-DwRh75JA.js +1 -0
  182. package/dist/dashboard/public/assets/ebnf-CDyGwa7X.js +1 -0
  183. package/dist/dashboard/public/assets/ecl-Cabwm37j.js +1 -0
  184. package/dist/dashboard/public/assets/edges-96097737--BjsAXwD.js +4 -0
  185. package/dist/dashboard/public/assets/eiffel-CnydiIhH.js +1 -0
  186. package/dist/dashboard/public/assets/elm-vLlmbW-K.js +1 -0
  187. package/dist/dashboard/public/assets/erDiagram-0228fc6a-BLGuJz36.js +51 -0
  188. package/dist/dashboard/public/assets/erlang-BNw1qcRV.js +1 -0
  189. package/dist/dashboard/public/assets/factor-kuTfRLto.js +1 -0
  190. package/dist/dashboard/public/assets/fcl-Kvtd6kyn.js +1 -0
  191. package/dist/dashboard/public/assets/flowDb-c6c81e3f-C8vD2iEO.js +10 -0
  192. package/dist/dashboard/public/assets/flowDiagram-50d868cf-BhxgVmOU.js +4 -0
  193. package/dist/dashboard/public/assets/flowDiagram-v2-4f6560a1-DvKCh0ha.js +1 -0
  194. package/dist/dashboard/public/assets/flowchart-elk-definition-6af322e1-CxOZDcEC.js +139 -0
  195. package/dist/dashboard/public/assets/forth-Ffai-XNe.js +1 -0
  196. package/dist/dashboard/public/assets/fortran-DYz_wnZ1.js +1 -0
  197. package/dist/dashboard/public/assets/ganttDiagram-a2739b55-vP9JOLba.js +257 -0
  198. package/dist/dashboard/public/assets/gas-Bneqetm1.js +1 -0
  199. package/dist/dashboard/public/assets/gherkin-heZmZLOM.js +1 -0
  200. package/dist/dashboard/public/assets/gitGraphDiagram-82fe8481-Cw0sm0i1.js +70 -0
  201. package/dist/dashboard/public/assets/graph-DKTWMcEG.js +1 -0
  202. package/dist/dashboard/public/assets/groovy-D9Dt4D0W.js +1 -0
  203. package/dist/dashboard/public/assets/haskell-Cw1EW3IL.js +1 -0
  204. package/dist/dashboard/public/assets/haxe-H-WmDvRZ.js +1 -0
  205. package/dist/dashboard/public/assets/http-DBlCnlav.js +1 -0
  206. package/dist/dashboard/public/assets/idl-BEugSyMb.js +1 -0
  207. package/dist/dashboard/public/assets/index-1zJPiVa8.js +3 -0
  208. package/dist/dashboard/public/assets/index-5325376f-DWs4kCT4.js +1 -0
  209. package/dist/dashboard/public/assets/index-BITJ9OoM.js +1 -0
  210. package/dist/dashboard/public/assets/index-C38JlXWp.js +1 -0
  211. package/dist/dashboard/public/assets/index-CCjPelL2.js +2 -0
  212. package/dist/dashboard/public/assets/index-CD9WQNmE.js +1 -0
  213. package/dist/dashboard/public/assets/index-CU7K5Zcb.js +1 -0
  214. package/dist/dashboard/public/assets/index-CXQVOhJV.js +1 -0
  215. package/dist/dashboard/public/assets/index-CXcaRrZ2.js +1 -0
  216. package/dist/dashboard/public/assets/index-ChLAL6g5.css +1 -0
  217. package/dist/dashboard/public/assets/index-D0o1vVOe.js +7 -0
  218. package/dist/dashboard/public/assets/index-DCsxqRvu.js +1 -0
  219. package/dist/dashboard/public/assets/index-DL3iiiRz.js +1 -0
  220. package/dist/dashboard/public/assets/index-DMv2_K2V.js +1 -0
  221. package/dist/dashboard/public/assets/index-DX7EEJ21.js +1 -0
  222. package/dist/dashboard/public/assets/index-Dey_HIH7.js +1 -0
  223. package/dist/dashboard/public/assets/index-DzDTRLhf.js +1 -0
  224. package/dist/dashboard/public/assets/index-OePkEWBg.js +1 -0
  225. package/dist/dashboard/public/assets/index-_d82jdTP.js +1 -0
  226. package/dist/dashboard/public/assets/index-yCKz4OXA.js +319 -0
  227. package/dist/dashboard/public/assets/infoDiagram-8eee0895-BRq08fZf.js +7 -0
  228. package/dist/dashboard/public/assets/init-Gi6I4Gst.js +1 -0
  229. package/dist/dashboard/public/assets/javascript-iXu5QeM3.js +1 -0
  230. package/dist/dashboard/public/assets/journeyDiagram-c64418c1-Cf8D2OC8.js +139 -0
  231. package/dist/dashboard/public/assets/julia-DuME0IfC.js +1 -0
  232. package/dist/dashboard/public/assets/katex-XbL3y5x-.js +261 -0
  233. package/dist/dashboard/public/assets/layout-CVdidYA-.js +1 -0
  234. package/dist/dashboard/public/assets/line-BdckgA27.js +1 -0
  235. package/dist/dashboard/public/assets/linear-C9Nh3JLa.js +1 -0
  236. package/dist/dashboard/public/assets/livescript-BwQOo05w.js +1 -0
  237. package/dist/dashboard/public/assets/lua-BgMRiT3U.js +1 -0
  238. package/dist/dashboard/public/assets/mathematica-DTrFuWx2.js +1 -0
  239. package/dist/dashboard/public/assets/mbox-CNhZ1qSd.js +1 -0
  240. package/dist/dashboard/public/assets/mindmap-definition-8da855dc-CK-y1AmO.js +415 -0
  241. package/dist/dashboard/public/assets/mirc-CjQqDB4T.js +1 -0
  242. package/dist/dashboard/public/assets/mllike-CXdrOF99.js +1 -0
  243. package/dist/dashboard/public/assets/modelica-Dc1JOy9r.js +1 -0
  244. package/dist/dashboard/public/assets/mscgen-BA5vi2Kp.js +1 -0
  245. package/dist/dashboard/public/assets/mumps-BT43cFF4.js +1 -0
  246. package/dist/dashboard/public/assets/nginx-DdIZxoE0.js +1 -0
  247. package/dist/dashboard/public/assets/nsis-LdVXkNf5.js +1 -0
  248. package/dist/dashboard/public/assets/ntriples-BfvgReVJ.js +1 -0
  249. package/dist/dashboard/public/assets/octave-Ck1zUtKM.js +1 -0
  250. package/dist/dashboard/public/assets/ordinal-Cboi1Yqb.js +1 -0
  251. package/dist/dashboard/public/assets/oz-BzwKVEFT.js +1 -0
  252. package/dist/dashboard/public/assets/pascal--L3eBynH.js +1 -0
  253. package/dist/dashboard/public/assets/path-CbwjOpE9.js +1 -0
  254. package/dist/dashboard/public/assets/perl-CdXCOZ3F.js +1 -0
  255. package/dist/dashboard/public/assets/pieDiagram-a8764435-T8V0JN2R.js +35 -0
  256. package/dist/dashboard/public/assets/pig-CevX1Tat.js +1 -0
  257. package/dist/dashboard/public/assets/powershell-CFHJl5sT.js +1 -0
  258. package/dist/dashboard/public/assets/properties-C78fOPTZ.js +1 -0
  259. package/dist/dashboard/public/assets/protobuf-ChK-085T.js +1 -0
  260. package/dist/dashboard/public/assets/pug-DeIclll2.js +1 -0
  261. package/dist/dashboard/public/assets/puppet-DMA9R1ak.js +1 -0
  262. package/dist/dashboard/public/assets/python-BuPzkPfP.js +1 -0
  263. package/dist/dashboard/public/assets/q-pXgVlZs6.js +1 -0
  264. package/dist/dashboard/public/assets/quadrantDiagram-1e28029f-CmtVsb5L.js +7 -0
  265. package/dist/dashboard/public/assets/r-B6wPVr8A.js +1 -0
  266. package/dist/dashboard/public/assets/requirementDiagram-08caed73-BUcTnzDl.js +52 -0
  267. package/dist/dashboard/public/assets/rpm-CTu-6PCP.js +1 -0
  268. package/dist/dashboard/public/assets/ruby-B2Rjki9n.js +1 -0
  269. package/dist/dashboard/public/assets/sankeyDiagram-a04cb91d-FswuxQ9M.js +8 -0
  270. package/dist/dashboard/public/assets/sas-B4kiWyti.js +1 -0
  271. package/dist/dashboard/public/assets/scheme-C41bIUwD.js +1 -0
  272. package/dist/dashboard/public/assets/sequenceDiagram-c5b8d532-BJQ15rhX.js +122 -0
  273. package/dist/dashboard/public/assets/shell-CjFT_Tl9.js +1 -0
  274. package/dist/dashboard/public/assets/sieve-C3Gn_uJK.js +1 -0
  275. package/dist/dashboard/public/assets/simple-mode-GW_nhZxv.js +1 -0
  276. package/dist/dashboard/public/assets/smalltalk-CnHTOXQT.js +1 -0
  277. package/dist/dashboard/public/assets/solr-DehyRSwq.js +1 -0
  278. package/dist/dashboard/public/assets/sparql-DkYu6x3z.js +1 -0
  279. package/dist/dashboard/public/assets/spreadsheet-BCZA_wO0.js +1 -0
  280. package/dist/dashboard/public/assets/sql-D0XecflT.js +1 -0
  281. package/dist/dashboard/public/assets/stateDiagram-1ecb1508-BfyE0DYv.js +1 -0
  282. package/dist/dashboard/public/assets/stateDiagram-v2-c2b004d7-pcGOYyiW.js +1 -0
  283. package/dist/dashboard/public/assets/stex-C3f8Ysf7.js +1 -0
  284. package/dist/dashboard/public/assets/styles-b4e223ce--lUviH7V.js +160 -0
  285. package/dist/dashboard/public/assets/styles-ca3715f6-BXbrD1Av.js +207 -0
  286. package/dist/dashboard/public/assets/styles-d45a18b0-GyiMrLKu.js +116 -0
  287. package/dist/dashboard/public/assets/stylus-B533Al4x.js +1 -0
  288. package/dist/dashboard/public/assets/svgDrawCommon-b86b1483-DI4Z1GTS.js +1 -0
  289. package/dist/dashboard/public/assets/swift-BzpIVaGY.js +1 -0
  290. package/dist/dashboard/public/assets/tcl-DVfN8rqt.js +1 -0
  291. package/dist/dashboard/public/assets/textile-CnDTJFAw.js +1 -0
  292. package/dist/dashboard/public/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  293. package/dist/dashboard/public/assets/tiki-DGYXhP31.js +1 -0
  294. package/dist/dashboard/public/assets/timeline-definition-faaaa080-B1IgohU4.js +61 -0
  295. package/dist/dashboard/public/assets/toml-Bm5Em-hy.js +1 -0
  296. package/dist/dashboard/public/assets/troff-wAsdV37c.js +1 -0
  297. package/dist/dashboard/public/assets/ttcn-CfJYG6tj.js +1 -0
  298. package/dist/dashboard/public/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  299. package/dist/dashboard/public/assets/turtle-B1tBg_DP.js +1 -0
  300. package/dist/dashboard/public/assets/vb-CmGdzxic.js +1 -0
  301. package/dist/dashboard/public/assets/vbscript-BuJXcnF6.js +1 -0
  302. package/dist/dashboard/public/assets/velocity-D8B20fx6.js +1 -0
  303. package/dist/dashboard/public/assets/verilog-C6RDOZhf.js +1 -0
  304. package/dist/dashboard/public/assets/vhdl-lSbBsy5d.js +1 -0
  305. package/dist/dashboard/public/assets/webidl-ZXfAyPTL.js +1 -0
  306. package/dist/dashboard/public/assets/xquery-DzFWVndE.js +1 -0
  307. package/dist/dashboard/public/assets/xychartDiagram-f5964ef8-B5oRDe_I.js +7 -0
  308. package/dist/dashboard/public/assets/yacas-BJ4BC0dw.js +1 -0
  309. package/dist/dashboard/public/assets/z80-Hz9HOZM7.js +1 -0
  310. package/dist/dashboard/public/claude-icon-dark.svg +1 -0
  311. package/dist/dashboard/public/claude-icon.svg +1 -0
  312. package/dist/dashboard/public/index.html +16 -0
  313. package/dist/dashboard/settings-manager.d.ts +47 -0
  314. package/dist/dashboard/settings-manager.d.ts.map +1 -0
  315. package/dist/dashboard/settings-manager.js +180 -0
  316. package/dist/dashboard/settings-manager.js.map +1 -0
  317. package/dist/dashboard/utils.d.ts +31 -0
  318. package/dist/dashboard/utils.d.ts.map +1 -0
  319. package/dist/dashboard/utils.js +102 -0
  320. package/dist/dashboard/utils.js.map +1 -0
  321. package/dist/dashboard/watcher.d.ts +32 -0
  322. package/dist/dashboard/watcher.d.ts.map +1 -0
  323. package/dist/dashboard/watcher.js +173 -0
  324. package/dist/dashboard/watcher.js.map +1 -0
  325. package/dist/index.d.ts +13 -0
  326. package/dist/index.d.ts.map +1 -0
  327. package/dist/index.js +380 -0
  328. package/dist/index.js.map +1 -0
  329. package/dist/markdown/templates/design-template.md +96 -0
  330. package/dist/markdown/templates/product-template.md +51 -0
  331. package/dist/markdown/templates/requirements-template.md +50 -0
  332. package/dist/markdown/templates/structure-template.md +145 -0
  333. package/dist/markdown/templates/tasks-template.md +139 -0
  334. package/dist/markdown/templates/tech-template.md +99 -0
  335. package/dist/prompts/create-spec.d.ts +3 -0
  336. package/dist/prompts/create-spec.d.ts.map +1 -0
  337. package/dist/prompts/create-spec.js +93 -0
  338. package/dist/prompts/create-spec.js.map +1 -0
  339. package/dist/prompts/create-steering-doc.d.ts +3 -0
  340. package/dist/prompts/create-steering-doc.d.ts.map +1 -0
  341. package/dist/prompts/create-steering-doc.js +73 -0
  342. package/dist/prompts/create-steering-doc.js.map +1 -0
  343. package/dist/prompts/implement-task.d.ts +3 -0
  344. package/dist/prompts/implement-task.d.ts.map +1 -0
  345. package/dist/prompts/implement-task.js +173 -0
  346. package/dist/prompts/implement-task.js.map +1 -0
  347. package/dist/prompts/index.d.ts +15 -0
  348. package/dist/prompts/index.d.ts.map +1 -0
  349. package/dist/prompts/index.js +49 -0
  350. package/dist/prompts/index.js.map +1 -0
  351. package/dist/prompts/inject-spec-workflow-guide.d.ts +3 -0
  352. package/dist/prompts/inject-spec-workflow-guide.d.ts.map +1 -0
  353. package/dist/prompts/inject-spec-workflow-guide.js +47 -0
  354. package/dist/prompts/inject-spec-workflow-guide.js.map +1 -0
  355. package/dist/prompts/inject-steering-guide.d.ts +3 -0
  356. package/dist/prompts/inject-steering-guide.d.ts.map +1 -0
  357. package/dist/prompts/inject-steering-guide.js +51 -0
  358. package/dist/prompts/inject-steering-guide.js.map +1 -0
  359. package/dist/prompts/refresh-tasks.d.ts +3 -0
  360. package/dist/prompts/refresh-tasks.d.ts.map +1 -0
  361. package/dist/prompts/refresh-tasks.js +224 -0
  362. package/dist/prompts/refresh-tasks.js.map +1 -0
  363. package/dist/prompts/spec-status.d.ts +3 -0
  364. package/dist/prompts/spec-status.d.ts.map +1 -0
  365. package/dist/prompts/spec-status.js +75 -0
  366. package/dist/prompts/spec-status.js.map +1 -0
  367. package/dist/prompts/types.d.ts +13 -0
  368. package/dist/prompts/types.d.ts.map +1 -0
  369. package/dist/prompts/types.js +2 -0
  370. package/dist/prompts/types.js.map +1 -0
  371. package/dist/server.d.ts +17 -0
  372. package/dist/server.d.ts.map +1 -0
  373. package/dist/server.js +175 -0
  374. package/dist/server.js.map +1 -0
  375. package/dist/tools/__tests__/projectPath.test.d.ts +2 -0
  376. package/dist/tools/__tests__/projectPath.test.d.ts.map +1 -0
  377. package/dist/tools/__tests__/projectPath.test.js +187 -0
  378. package/dist/tools/__tests__/projectPath.test.js.map +1 -0
  379. package/dist/tools/approvals.d.ts +14 -0
  380. package/dist/tools/approvals.d.ts.map +1 -0
  381. package/dist/tools/approvals.js +490 -0
  382. package/dist/tools/approvals.js.map +1 -0
  383. package/dist/tools/index.d.ts +5 -0
  384. package/dist/tools/index.d.ts.map +1 -0
  385. package/dist/tools/index.js +52 -0
  386. package/dist/tools/index.js.map +1 -0
  387. package/dist/tools/log-implementation.d.ts +5 -0
  388. package/dist/tools/log-implementation.d.ts.map +1 -0
  389. package/dist/tools/log-implementation.js +397 -0
  390. package/dist/tools/log-implementation.js.map +1 -0
  391. package/dist/tools/spec-status.d.ts +5 -0
  392. package/dist/tools/spec-status.d.ts.map +1 -0
  393. package/dist/tools/spec-status.js +178 -0
  394. package/dist/tools/spec-status.js.map +1 -0
  395. package/dist/tools/spec-workflow-guide.d.ts +5 -0
  396. package/dist/tools/spec-workflow-guide.d.ts.map +1 -0
  397. package/dist/tools/spec-workflow-guide.js +291 -0
  398. package/dist/tools/spec-workflow-guide.js.map +1 -0
  399. package/dist/tools/steering-guide.d.ts +5 -0
  400. package/dist/tools/steering-guide.d.ts.map +1 -0
  401. package/dist/tools/steering-guide.js +192 -0
  402. package/dist/tools/steering-guide.js.map +1 -0
  403. package/dist/types.d.ts +172 -0
  404. package/dist/types.d.ts.map +1 -0
  405. package/dist/types.js +13 -0
  406. package/dist/types.js.map +1 -0
  407. package/package.json +105 -0
@@ -0,0 +1,1313 @@
1
+ import fastify from 'fastify';
2
+ import fastifyStatic from '@fastify/static';
3
+ import fastifyWebsocket from '@fastify/websocket';
4
+ import fastifyCors from '@fastify/cors';
5
+ import { join, dirname } from 'path';
6
+ import { readFile } from 'fs/promises';
7
+ import { promises as fs } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import open from 'open';
10
+ import { WebSocket } from 'ws';
11
+ import { validateAndCheckPort, DASHBOARD_TEST_MESSAGE } from './utils.js';
12
+ import { parseTasksFromMarkdown } from '../core/task-parser.js';
13
+ import { ProjectManager } from './project-manager.js';
14
+ import { JobScheduler } from './job-scheduler.js';
15
+ import { ImplementationLogManager } from './implementation-log-manager.js';
16
+ import { DashboardSessionManager } from '../core/dashboard-session.js';
17
+ import { getSecurityConfig, RateLimiter, AuditLogger, createSecurityHeadersMiddleware, getCorsConfig, isLocalhostAddress } from '../core/security-utils.js';
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+ export class MultiProjectDashboardServer {
21
+ app;
22
+ projectManager;
23
+ jobScheduler;
24
+ sessionManager;
25
+ options;
26
+ bindAddress;
27
+ allowExternalAccess;
28
+ securityConfig;
29
+ rateLimiter;
30
+ auditLogger;
31
+ actualPort = 0;
32
+ clients = new Set();
33
+ packageVersion = 'unknown';
34
+ heartbeatInterval;
35
+ HEARTBEAT_INTERVAL_MS = 30000;
36
+ HEARTBEAT_TIMEOUT_MS = 10000;
37
+ // Debounce spec broadcasts to coalesce rapid updates
38
+ pendingSpecBroadcasts = new Map();
39
+ SPEC_BROADCAST_DEBOUNCE_MS = 300;
40
+ constructor(options = {}) {
41
+ this.options = options;
42
+ this.projectManager = new ProjectManager();
43
+ this.jobScheduler = new JobScheduler(this.projectManager);
44
+ this.sessionManager = new DashboardSessionManager();
45
+ // Initialize network binding configuration
46
+ this.bindAddress = options.bindAddress || '127.0.0.1';
47
+ this.allowExternalAccess = options.allowExternalAccess || false;
48
+ // Validate network binding security
49
+ if (!isLocalhostAddress(this.bindAddress) && !this.allowExternalAccess) {
50
+ throw new Error(`SECURITY ERROR: Binding to '${this.bindAddress}' (non-localhost) requires explicit allowExternalAccess=true. ` +
51
+ 'This exposes your dashboard to network access. Use 127.0.0.1 for localhost-only access.');
52
+ }
53
+ // Initialize security features configuration with the actual port
54
+ // This ensures CORS allowedOrigins and CSP are port-aware
55
+ this.securityConfig = getSecurityConfig(options.security, options.port);
56
+ this.app = fastify({ logger: false });
57
+ }
58
+ async start() {
59
+ // Security warning if binding to non-localhost address
60
+ if (!isLocalhostAddress(this.bindAddress)) {
61
+ console.error('');
62
+ console.error('⚠️ ═══════════════════════════════════════════════════════════');
63
+ console.error(`⚠️ SECURITY WARNING: Dashboard binding to ${this.bindAddress}`);
64
+ console.error('⚠️ This exposes your dashboard to network-based attacks!');
65
+ console.error('⚠️ Recommendation: Use 127.0.0.1 for localhost-only access');
66
+ console.error('⚠️ ═══════════════════════════════════════════════════════════');
67
+ console.error('');
68
+ }
69
+ // Display security status
70
+ console.error('🔒 Security Configuration:');
71
+ console.error(` - Bind Address: ${this.bindAddress}`);
72
+ console.error(` - Rate Limiting: ${this.securityConfig.rateLimitEnabled ? 'ENABLED ✓' : 'DISABLED ⚠️'}`);
73
+ console.error(` - Audit Logging: ${this.securityConfig.auditLogEnabled ? 'ENABLED ✓' : 'DISABLED ⚠️'}`);
74
+ console.error(` - CORS: ${this.securityConfig.corsEnabled ? 'ENABLED ✓' : 'DISABLED ⚠️'}`);
75
+ console.error(` - Allowed Origins: ${this.securityConfig.allowedOrigins.join(', ')}`);
76
+ console.error('');
77
+ // Fetch package version once at startup
78
+ try {
79
+ const response = await fetch('https://registry.npmjs.org/@pimzino/spec-workflow-mcp/latest');
80
+ if (response.ok) {
81
+ const packageInfo = await response.json();
82
+ this.packageVersion = packageInfo.version || 'unknown';
83
+ }
84
+ }
85
+ catch {
86
+ // Fallback to local package.json version if npm request fails
87
+ try {
88
+ const packageJsonPath = join(__dirname, '..', '..', 'package.json');
89
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8');
90
+ const packageJson = JSON.parse(packageJsonContent);
91
+ this.packageVersion = packageJson.version || 'unknown';
92
+ }
93
+ catch {
94
+ // Keep default 'unknown' if both npm and local package.json fail
95
+ }
96
+ }
97
+ // Initialize security components
98
+ if (this.securityConfig.rateLimitEnabled) {
99
+ this.rateLimiter = new RateLimiter(this.securityConfig);
100
+ }
101
+ if (this.securityConfig.auditLogEnabled) {
102
+ this.auditLogger = new AuditLogger(this.securityConfig);
103
+ await this.auditLogger.initialize();
104
+ }
105
+ // Initialize project manager
106
+ await this.projectManager.initialize();
107
+ // Initialize job scheduler
108
+ await this.jobScheduler.initialize();
109
+ // Register CORS plugin if enabled
110
+ const corsConfig = getCorsConfig(this.securityConfig);
111
+ if (corsConfig !== false) {
112
+ await this.app.register(fastifyCors, corsConfig);
113
+ }
114
+ // Register security middleware (apply to all routes)
115
+ // Pass the actual port for CSP connect-src WebSocket configuration
116
+ this.app.addHook('onRequest', createSecurityHeadersMiddleware(this.options.port));
117
+ if (this.rateLimiter) {
118
+ this.app.addHook('onRequest', this.rateLimiter.middleware());
119
+ }
120
+ if (this.auditLogger) {
121
+ this.app.addHook('onRequest', this.auditLogger.middleware());
122
+ }
123
+ // Register plugins
124
+ await this.app.register(fastifyStatic, {
125
+ root: join(__dirname, 'public'),
126
+ prefix: '/',
127
+ });
128
+ await this.app.register(fastifyWebsocket);
129
+ // WebSocket endpoint for real-time updates
130
+ const self = this;
131
+ await this.app.register(async function (fastify) {
132
+ fastify.get('/ws', { websocket: true }, (connection, req) => {
133
+ const socket = connection.socket;
134
+ // Get projectId from query parameter
135
+ const url = new URL(req.url || '', `http://${req.headers.host}`);
136
+ const projectId = url.searchParams.get('projectId') || undefined;
137
+ connection.projectId = projectId;
138
+ connection.isAlive = true;
139
+ self.clients.add(connection);
140
+ // Handle pong for heartbeat
141
+ socket.on('pong', () => {
142
+ connection.isAlive = true;
143
+ });
144
+ // Send initial state for the requested project
145
+ if (projectId) {
146
+ const project = self.projectManager.getProject(projectId);
147
+ if (project) {
148
+ Promise.all([
149
+ project.parser.getAllSpecs(),
150
+ project.approvalStorage.getAllPendingApprovals()
151
+ ])
152
+ .then(([specs, approvals]) => {
153
+ socket.send(JSON.stringify({
154
+ type: 'initial',
155
+ projectId,
156
+ data: { specs, approvals },
157
+ }));
158
+ })
159
+ .catch((error) => {
160
+ console.error('Error getting initial data:', error);
161
+ });
162
+ }
163
+ }
164
+ // Send projects list
165
+ socket.send(JSON.stringify({
166
+ type: 'projects-update',
167
+ data: { projects: self.projectManager.getProjectsList() }
168
+ }));
169
+ // Handle client disconnect
170
+ const cleanup = () => {
171
+ self.clients.delete(connection);
172
+ socket.removeAllListeners();
173
+ };
174
+ socket.on('close', cleanup);
175
+ socket.on('error', cleanup);
176
+ socket.on('disconnect', cleanup);
177
+ socket.on('end', cleanup);
178
+ // Handle subscription messages
179
+ socket.on('message', (data) => {
180
+ try {
181
+ const msg = JSON.parse(data.toString());
182
+ if (msg.type === 'subscribe' && msg.projectId) {
183
+ connection.projectId = msg.projectId;
184
+ // Send initial data for new subscription
185
+ const project = self.projectManager.getProject(msg.projectId);
186
+ if (project) {
187
+ Promise.all([
188
+ project.parser.getAllSpecs(),
189
+ project.approvalStorage.getAllPendingApprovals()
190
+ ])
191
+ .then(([specs, approvals]) => {
192
+ socket.send(JSON.stringify({
193
+ type: 'initial',
194
+ projectId: msg.projectId,
195
+ data: { specs, approvals },
196
+ }));
197
+ })
198
+ .catch((error) => {
199
+ console.error('Error getting initial data:', error);
200
+ });
201
+ }
202
+ }
203
+ }
204
+ catch (error) {
205
+ // Ignore invalid messages
206
+ }
207
+ });
208
+ });
209
+ });
210
+ // Serve Claude icon as favicon
211
+ this.app.get('/favicon.ico', async (request, reply) => {
212
+ return reply.sendFile('claude-icon.svg');
213
+ });
214
+ // Setup project manager event handlers
215
+ this.setupProjectManagerEvents();
216
+ // Register API routes
217
+ this.registerApiRoutes();
218
+ // Validate and set port (always provided by caller)
219
+ if (!this.options.port) {
220
+ throw new Error('Dashboard port must be specified');
221
+ }
222
+ await validateAndCheckPort(this.options.port, this.bindAddress);
223
+ this.actualPort = this.options.port;
224
+ // Start server with configured network binding
225
+ await this.app.listen({
226
+ port: this.actualPort,
227
+ host: this.bindAddress
228
+ });
229
+ // Start WebSocket heartbeat monitoring
230
+ this.startHeartbeat();
231
+ // Register dashboard in the session manager
232
+ const dashboardUrl = `http://localhost:${this.actualPort}`;
233
+ await this.sessionManager.registerDashboard(dashboardUrl, this.actualPort, process.pid);
234
+ // Open browser if requested
235
+ if (this.options.autoOpen) {
236
+ await open(dashboardUrl);
237
+ }
238
+ return dashboardUrl;
239
+ }
240
+ setupProjectManagerEvents() {
241
+ // Broadcast projects update when projects change
242
+ this.projectManager.on('projects-update', (projects) => {
243
+ this.broadcastToAll({
244
+ type: 'projects-update',
245
+ data: { projects }
246
+ });
247
+ });
248
+ // Broadcast spec changes (debounced per project to coalesce rapid updates)
249
+ this.projectManager.on('spec-change', (event) => {
250
+ const { projectId } = event;
251
+ // Clear existing pending broadcast for this project
252
+ const existingTimeout = this.pendingSpecBroadcasts.get(projectId);
253
+ if (existingTimeout) {
254
+ clearTimeout(existingTimeout);
255
+ }
256
+ // Schedule debounced broadcast
257
+ const timeout = setTimeout(async () => {
258
+ this.pendingSpecBroadcasts.delete(projectId);
259
+ try {
260
+ const project = this.projectManager.getProject(projectId);
261
+ if (project) {
262
+ const specs = await project.parser.getAllSpecs();
263
+ const archivedSpecs = await project.parser.getAllArchivedSpecs();
264
+ this.broadcastToProject(projectId, {
265
+ type: 'spec-update',
266
+ projectId,
267
+ data: { specs, archivedSpecs }
268
+ });
269
+ }
270
+ }
271
+ catch (error) {
272
+ console.error('Error broadcasting spec changes:', error);
273
+ // Don't propagate error to prevent event system crash
274
+ }
275
+ }, this.SPEC_BROADCAST_DEBOUNCE_MS);
276
+ this.pendingSpecBroadcasts.set(projectId, timeout);
277
+ });
278
+ // Broadcast task updates
279
+ this.projectManager.on('task-update', (event) => {
280
+ const { projectId, specName } = event;
281
+ this.broadcastTaskUpdate(projectId, specName);
282
+ });
283
+ // Broadcast steering changes
284
+ this.projectManager.on('steering-change', async (event) => {
285
+ try {
286
+ const { projectId, steeringStatus } = event;
287
+ this.broadcastToProject(projectId, {
288
+ type: 'steering-update',
289
+ projectId,
290
+ data: steeringStatus
291
+ });
292
+ }
293
+ catch (error) {
294
+ console.error('Error broadcasting steering changes:', error);
295
+ // Don't propagate error to prevent event system crash
296
+ }
297
+ });
298
+ // Broadcast approval changes
299
+ this.projectManager.on('approval-change', async (event) => {
300
+ try {
301
+ const { projectId } = event;
302
+ const project = this.projectManager.getProject(projectId);
303
+ if (project) {
304
+ const approvals = await project.approvalStorage.getAllPendingApprovals();
305
+ this.broadcastToProject(projectId, {
306
+ type: 'approval-update',
307
+ projectId,
308
+ data: approvals
309
+ });
310
+ }
311
+ }
312
+ catch (error) {
313
+ console.error('Error broadcasting approval changes:', error);
314
+ // Don't propagate error to prevent event system crash
315
+ }
316
+ });
317
+ }
318
+ registerApiRoutes() {
319
+ // Health check / test endpoint (used by utils.ts to detect running dashboard)
320
+ this.app.get('/api/test', async () => {
321
+ return { message: DASHBOARD_TEST_MESSAGE };
322
+ });
323
+ // Projects list
324
+ this.app.get('/api/projects/list', async () => {
325
+ return this.projectManager.getProjectsList();
326
+ });
327
+ // Add project manually
328
+ this.app.post('/api/projects/add', async (request, reply) => {
329
+ const { projectPath } = request.body;
330
+ if (!projectPath) {
331
+ return reply.code(400).send({ error: 'projectPath is required' });
332
+ }
333
+ try {
334
+ const projectId = await this.projectManager.addProjectByPath(projectPath);
335
+ return { projectId, success: true };
336
+ }
337
+ catch (error) {
338
+ return reply.code(500).send({ error: error.message });
339
+ }
340
+ });
341
+ // Remove project
342
+ this.app.delete('/api/projects/:projectId', async (request, reply) => {
343
+ const { projectId } = request.params;
344
+ try {
345
+ await this.projectManager.removeProjectById(projectId);
346
+ return { success: true };
347
+ }
348
+ catch (error) {
349
+ return reply.code(500).send({ error: error.message });
350
+ }
351
+ });
352
+ // Project info
353
+ this.app.get('/api/projects/:projectId/info', async (request, reply) => {
354
+ const { projectId } = request.params;
355
+ const project = this.projectManager.getProject(projectId);
356
+ if (!project) {
357
+ return reply.code(404).send({ error: 'Project not found' });
358
+ }
359
+ const steeringStatus = await project.parser.getProjectSteeringStatus();
360
+ return {
361
+ projectId,
362
+ projectName: project.projectName,
363
+ projectPath: project.originalProjectPath, // Return original path for display
364
+ steering: steeringStatus,
365
+ version: this.packageVersion
366
+ };
367
+ });
368
+ // Specs list
369
+ this.app.get('/api/projects/:projectId/specs', async (request, reply) => {
370
+ const { projectId } = request.params;
371
+ const project = this.projectManager.getProject(projectId);
372
+ if (!project) {
373
+ return reply.code(404).send({ error: 'Project not found' });
374
+ }
375
+ return await project.parser.getAllSpecs();
376
+ });
377
+ // Archived specs list
378
+ this.app.get('/api/projects/:projectId/specs/archived', async (request, reply) => {
379
+ const { projectId } = request.params;
380
+ const project = this.projectManager.getProject(projectId);
381
+ if (!project) {
382
+ return reply.code(404).send({ error: 'Project not found' });
383
+ }
384
+ return await project.parser.getAllArchivedSpecs();
385
+ });
386
+ // Get spec details
387
+ this.app.get('/api/projects/:projectId/specs/:name', async (request, reply) => {
388
+ const { projectId, name } = request.params;
389
+ const project = this.projectManager.getProject(projectId);
390
+ if (!project) {
391
+ return reply.code(404).send({ error: 'Project not found' });
392
+ }
393
+ const spec = await project.parser.getSpec(name);
394
+ if (!spec) {
395
+ return reply.code(404).send({ error: 'Spec not found' });
396
+ }
397
+ return spec;
398
+ });
399
+ // Get all spec documents
400
+ this.app.get('/api/projects/:projectId/specs/:name/all', async (request, reply) => {
401
+ const { projectId, name } = request.params;
402
+ const project = this.projectManager.getProject(projectId);
403
+ if (!project) {
404
+ return reply.code(404).send({ error: 'Project not found' });
405
+ }
406
+ const specDir = join(project.projectPath, '.spec-workflow', 'specs', name);
407
+ const documents = ['requirements', 'design', 'tasks'];
408
+ const result = {};
409
+ for (const doc of documents) {
410
+ const docPath = join(specDir, `${doc}.md`);
411
+ try {
412
+ const content = await readFile(docPath, 'utf-8');
413
+ const stats = await fs.stat(docPath);
414
+ result[doc] = {
415
+ content,
416
+ lastModified: stats.mtime.toISOString()
417
+ };
418
+ }
419
+ catch {
420
+ result[doc] = null;
421
+ }
422
+ }
423
+ return result;
424
+ });
425
+ // Get all archived spec documents
426
+ this.app.get('/api/projects/:projectId/specs/:name/all/archived', async (request, reply) => {
427
+ const { projectId, name } = request.params;
428
+ const project = this.projectManager.getProject(projectId);
429
+ if (!project) {
430
+ return reply.code(404).send({ error: 'Project not found' });
431
+ }
432
+ // Use archive path instead of active specs path
433
+ const specDir = join(project.projectPath, '.spec-workflow', 'archive', 'specs', name);
434
+ const documents = ['requirements', 'design', 'tasks'];
435
+ const result = {};
436
+ for (const doc of documents) {
437
+ const docPath = join(specDir, `${doc}.md`);
438
+ try {
439
+ const content = await readFile(docPath, 'utf-8');
440
+ const stats = await fs.stat(docPath);
441
+ result[doc] = {
442
+ content,
443
+ lastModified: stats.mtime.toISOString()
444
+ };
445
+ }
446
+ catch {
447
+ result[doc] = null;
448
+ }
449
+ }
450
+ return result;
451
+ });
452
+ // Save spec document
453
+ this.app.put('/api/projects/:projectId/specs/:name/:document', async (request, reply) => {
454
+ const { projectId, name, document } = request.params;
455
+ const { content } = request.body;
456
+ const project = this.projectManager.getProject(projectId);
457
+ if (!project) {
458
+ return reply.code(404).send({ error: 'Project not found' });
459
+ }
460
+ const allowedDocs = ['requirements', 'design', 'tasks'];
461
+ if (!allowedDocs.includes(document)) {
462
+ return reply.code(400).send({ error: 'Invalid document type' });
463
+ }
464
+ if (typeof content !== 'string') {
465
+ return reply.code(400).send({ error: 'Content must be a string' });
466
+ }
467
+ const docPath = join(project.projectPath, '.spec-workflow', 'specs', name, `${document}.md`);
468
+ try {
469
+ const specDir = join(project.projectPath, '.spec-workflow', 'specs', name);
470
+ await fs.mkdir(specDir, { recursive: true });
471
+ await fs.writeFile(docPath, content, 'utf-8');
472
+ return { success: true, message: 'Document saved successfully' };
473
+ }
474
+ catch (error) {
475
+ return reply.code(500).send({ error: `Failed to save document: ${error.message}` });
476
+ }
477
+ });
478
+ // Archive spec
479
+ this.app.post('/api/projects/:projectId/specs/:name/archive', async (request, reply) => {
480
+ const { projectId, name } = request.params;
481
+ const project = this.projectManager.getProject(projectId);
482
+ if (!project) {
483
+ return reply.code(404).send({ error: 'Project not found' });
484
+ }
485
+ try {
486
+ await project.archiveService.archiveSpec(name);
487
+ return { success: true, message: `Spec '${name}' archived successfully` };
488
+ }
489
+ catch (error) {
490
+ return reply.code(400).send({ error: error.message });
491
+ }
492
+ });
493
+ // Unarchive spec
494
+ this.app.post('/api/projects/:projectId/specs/:name/unarchive', async (request, reply) => {
495
+ const { projectId, name } = request.params;
496
+ const project = this.projectManager.getProject(projectId);
497
+ if (!project) {
498
+ return reply.code(404).send({ error: 'Project not found' });
499
+ }
500
+ try {
501
+ await project.archiveService.unarchiveSpec(name);
502
+ return { success: true, message: `Spec '${name}' unarchived successfully` };
503
+ }
504
+ catch (error) {
505
+ return reply.code(400).send({ error: error.message });
506
+ }
507
+ });
508
+ // Get approvals
509
+ this.app.get('/api/projects/:projectId/approvals', async (request, reply) => {
510
+ const { projectId } = request.params;
511
+ const project = this.projectManager.getProject(projectId);
512
+ if (!project) {
513
+ return reply.code(404).send({ error: 'Project not found' });
514
+ }
515
+ return await project.approvalStorage.getAllPendingApprovals();
516
+ });
517
+ // Get approval content
518
+ this.app.get('/api/projects/:projectId/approvals/:id/content', async (request, reply) => {
519
+ const { projectId, id } = request.params;
520
+ const project = this.projectManager.getProject(projectId);
521
+ if (!project) {
522
+ return reply.code(404).send({ error: 'Project not found' });
523
+ }
524
+ try {
525
+ const approval = await project.approvalStorage.getApproval(id);
526
+ if (!approval || !approval.filePath) {
527
+ return reply.code(404).send({ error: 'Approval not found or no file path' });
528
+ }
529
+ const candidateSet = new Set();
530
+ const p = approval.filePath;
531
+ const isAbsolutePath = p.startsWith('/') || p.match(/^[A-Za-z]:[\\\/]/);
532
+ if (!isAbsolutePath) {
533
+ // 1) Resolve against workspace/worktree first
534
+ candidateSet.add(join(project.workspacePath, p));
535
+ }
536
+ // 2) Absolute path as-is
537
+ if (isAbsolutePath) {
538
+ candidateSet.add(p);
539
+ }
540
+ if (!isAbsolutePath) {
541
+ // 3) Resolve against workflow root
542
+ candidateSet.add(join(project.projectPath, p));
543
+ // 4) Legacy fallback for historical paths
544
+ if (!p.includes('.spec-workflow')) {
545
+ candidateSet.add(join(project.projectPath, '.spec-workflow', p));
546
+ }
547
+ }
548
+ const candidates = Array.from(candidateSet);
549
+ let content = null;
550
+ let resolvedPath = null;
551
+ for (const candidate of candidates) {
552
+ try {
553
+ const data = await fs.readFile(candidate, 'utf-8');
554
+ content = data;
555
+ resolvedPath = candidate;
556
+ break;
557
+ }
558
+ catch {
559
+ // try next candidate
560
+ }
561
+ }
562
+ if (content == null) {
563
+ return reply.code(500).send({ error: `Failed to read file at any known location for ${approval.filePath}` });
564
+ }
565
+ return { content, filePath: resolvedPath || approval.filePath };
566
+ }
567
+ catch (error) {
568
+ return reply.code(500).send({ error: `Failed to read file: ${error.message}` });
569
+ }
570
+ });
571
+ // Approval actions (approve, reject, needs-revision)
572
+ this.app.post('/api/projects/:projectId/approvals/:id/:action', async (request, reply) => {
573
+ try {
574
+ const { projectId, id, action } = request.params;
575
+ const { response, annotations, comments } = (request.body || {});
576
+ const project = this.projectManager.getProject(projectId);
577
+ if (!project) {
578
+ return reply.code(404).send({ error: 'Project not found' });
579
+ }
580
+ const validActions = ['approve', 'reject', 'needs-revision'];
581
+ if (!validActions.includes(action)) {
582
+ return reply.code(400).send({ error: 'Invalid action' });
583
+ }
584
+ // Convert action name to status value
585
+ const actionToStatus = {
586
+ 'approve': 'approved',
587
+ 'reject': 'rejected',
588
+ 'needs-revision': 'needs-revision'
589
+ };
590
+ const status = actionToStatus[action];
591
+ await project.approvalStorage.updateApproval(id, status, response, annotations, comments);
592
+ return { success: true };
593
+ }
594
+ catch (error) {
595
+ return reply.code(500).send({ error: error.message || 'Internal server error' });
596
+ }
597
+ });
598
+ // Undo batch operations - revert items back to pending
599
+ // IMPORTANT: This route MUST be defined BEFORE the /batch/:action route
600
+ // because Fastify matches routes in order of registration
601
+ this.app.post('/api/projects/:projectId/approvals/batch/undo', async (request, reply) => {
602
+ const { projectId } = request.params;
603
+ const { ids } = request.body;
604
+ const project = this.projectManager.getProject(projectId);
605
+ if (!project) {
606
+ return reply.code(404).send({ error: 'Project not found' });
607
+ }
608
+ // Validate ids array
609
+ if (!Array.isArray(ids) || ids.length === 0) {
610
+ return reply.code(400).send({ error: 'ids must be a non-empty array' });
611
+ }
612
+ // Batch size limit
613
+ const BATCH_SIZE_LIMIT = 100;
614
+ if (ids.length > BATCH_SIZE_LIMIT) {
615
+ return reply.code(400).send({
616
+ error: `Batch size exceeds limit. Maximum ${BATCH_SIZE_LIMIT} items allowed.`
617
+ });
618
+ }
619
+ // Validate ID format
620
+ const idPattern = /^[a-zA-Z0-9_-]+$/;
621
+ const invalidIds = ids.filter(id => !idPattern.test(id));
622
+ if (invalidIds.length > 0) {
623
+ return reply.code(400).send({
624
+ error: `Invalid ID format: ${invalidIds.slice(0, 3).join(', ')}${invalidIds.length > 3 ? '...' : ''}`
625
+ });
626
+ }
627
+ // Process all undo operations with continue-on-error
628
+ const results = {
629
+ succeeded: [],
630
+ failed: []
631
+ };
632
+ for (const id of ids) {
633
+ try {
634
+ // Revert to pending status, clear response and respondedAt
635
+ await project.approvalStorage.revertToPending(id);
636
+ results.succeeded.push(id);
637
+ }
638
+ catch (error) {
639
+ results.failed.push({ id, error: error.message });
640
+ }
641
+ }
642
+ // Broadcast WebSocket update for successful undos
643
+ if (results.succeeded.length > 0) {
644
+ this.broadcastToProject(projectId, {
645
+ type: 'batch-approval-undo',
646
+ ids: results.succeeded,
647
+ count: results.succeeded.length
648
+ });
649
+ }
650
+ return {
651
+ success: results.failed.length === 0,
652
+ total: ids.length,
653
+ succeeded: results.succeeded,
654
+ failed: results.failed
655
+ };
656
+ });
657
+ // Batch approval actions (approve, reject only - no batch needs-revision)
658
+ this.app.post('/api/projects/:projectId/approvals/batch/:action', async (request, reply) => {
659
+ const { projectId, action } = request.params;
660
+ const { ids, response } = request.body;
661
+ const project = this.projectManager.getProject(projectId);
662
+ if (!project) {
663
+ return reply.code(404).send({ error: 'Project not found' });
664
+ }
665
+ // Only allow approve and reject for batch operations (UX recommendation)
666
+ const validBatchActions = ['approve', 'reject'];
667
+ if (!validBatchActions.includes(action)) {
668
+ return reply.code(400).send({
669
+ error: 'Invalid batch action. Only "approve" and "reject" are allowed for batch operations.'
670
+ });
671
+ }
672
+ // Validate ids array
673
+ if (!Array.isArray(ids) || ids.length === 0) {
674
+ return reply.code(400).send({ error: 'ids must be a non-empty array' });
675
+ }
676
+ // Batch size limit (PE recommendation)
677
+ const BATCH_SIZE_LIMIT = 100;
678
+ if (ids.length > BATCH_SIZE_LIMIT) {
679
+ return reply.code(400).send({
680
+ error: `Batch size exceeds limit. Maximum ${BATCH_SIZE_LIMIT} items allowed.`
681
+ });
682
+ }
683
+ // Validate ID format (PE recommendation - alphanumeric with hyphens/underscores)
684
+ const idPattern = /^[a-zA-Z0-9_-]+$/;
685
+ const invalidIds = ids.filter(id => !idPattern.test(id));
686
+ if (invalidIds.length > 0) {
687
+ return reply.code(400).send({
688
+ error: `Invalid ID format: ${invalidIds.slice(0, 3).join(', ')}${invalidIds.length > 3 ? '...' : ''}`
689
+ });
690
+ }
691
+ const actionToStatus = {
692
+ 'approve': 'approved',
693
+ 'reject': 'rejected'
694
+ };
695
+ const status = actionToStatus[action];
696
+ const batchResponse = response || `Batch ${action}d`;
697
+ // Process all approvals with continue-on-error (PE recommendation)
698
+ const results = {
699
+ succeeded: [],
700
+ failed: []
701
+ };
702
+ // Debounce WebSocket broadcasts - collect all updates first (use results.succeeded)
703
+ for (const id of ids) {
704
+ try {
705
+ await project.approvalStorage.updateApproval(id, status, batchResponse);
706
+ results.succeeded.push(id);
707
+ }
708
+ catch (error) {
709
+ results.failed.push({ id, error: error.message });
710
+ }
711
+ }
712
+ // Single consolidated WebSocket broadcast for all successful updates
713
+ if (results.succeeded.length > 0) {
714
+ this.broadcastToProject(projectId, {
715
+ type: 'batch-approval-update',
716
+ action: action,
717
+ ids: results.succeeded,
718
+ count: results.succeeded.length
719
+ });
720
+ }
721
+ return {
722
+ success: results.failed.length === 0,
723
+ total: ids.length,
724
+ succeeded: results.succeeded,
725
+ failed: results.failed
726
+ };
727
+ });
728
+ // Get all snapshots for an approval
729
+ this.app.get('/api/projects/:projectId/approvals/:id/snapshots', async (request, reply) => {
730
+ const { projectId, id } = request.params;
731
+ const project = this.projectManager.getProject(projectId);
732
+ if (!project) {
733
+ return reply.code(404).send({ error: 'Project not found' });
734
+ }
735
+ try {
736
+ const snapshots = await project.approvalStorage.getSnapshots(id);
737
+ return snapshots;
738
+ }
739
+ catch (error) {
740
+ return reply.code(500).send({ error: `Failed to get snapshots: ${error.message}` });
741
+ }
742
+ });
743
+ // Get specific snapshot version for an approval
744
+ this.app.get('/api/projects/:projectId/approvals/:id/snapshots/:version', async (request, reply) => {
745
+ const { projectId, id, version } = request.params;
746
+ const project = this.projectManager.getProject(projectId);
747
+ if (!project) {
748
+ return reply.code(404).send({ error: 'Project not found' });
749
+ }
750
+ try {
751
+ const versionNum = parseInt(version, 10);
752
+ if (isNaN(versionNum)) {
753
+ return reply.code(400).send({ error: 'Invalid version number' });
754
+ }
755
+ const snapshot = await project.approvalStorage.getSnapshot(id, versionNum);
756
+ if (!snapshot) {
757
+ return reply.code(404).send({ error: `Snapshot version ${version} not found` });
758
+ }
759
+ return snapshot;
760
+ }
761
+ catch (error) {
762
+ return reply.code(500).send({ error: `Failed to get snapshot: ${error.message}` });
763
+ }
764
+ });
765
+ // Get diff between two versions or between version and current
766
+ this.app.get('/api/projects/:projectId/approvals/:id/diff', async (request, reply) => {
767
+ const { projectId, id } = request.params;
768
+ const { from, to } = request.query;
769
+ const project = this.projectManager.getProject(projectId);
770
+ if (!project) {
771
+ return reply.code(404).send({ error: 'Project not found' });
772
+ }
773
+ if (!from) {
774
+ return reply.code(400).send({ error: 'from parameter is required' });
775
+ }
776
+ try {
777
+ const fromVersion = parseInt(from, 10);
778
+ if (isNaN(fromVersion)) {
779
+ return reply.code(400).send({ error: 'Invalid from version number' });
780
+ }
781
+ let toVersion;
782
+ if (to === 'current' || to === undefined) {
783
+ toVersion = 'current';
784
+ }
785
+ else {
786
+ const toVersionNum = parseInt(to, 10);
787
+ if (isNaN(toVersionNum)) {
788
+ return reply.code(400).send({ error: 'Invalid to version number' });
789
+ }
790
+ toVersion = toVersionNum;
791
+ }
792
+ const diff = await project.approvalStorage.compareSnapshots(id, fromVersion, toVersion);
793
+ return diff;
794
+ }
795
+ catch (error) {
796
+ return reply.code(500).send({ error: `Failed to compute diff: ${error.message}` });
797
+ }
798
+ });
799
+ // Manual snapshot capture
800
+ this.app.post('/api/projects/:projectId/approvals/:id/snapshot', async (request, reply) => {
801
+ const { projectId, id } = request.params;
802
+ const project = this.projectManager.getProject(projectId);
803
+ if (!project) {
804
+ return reply.code(404).send({ error: 'Project not found' });
805
+ }
806
+ try {
807
+ await project.approvalStorage.captureSnapshot(id, 'manual');
808
+ return { success: true, message: 'Snapshot captured successfully' };
809
+ }
810
+ catch (error) {
811
+ return reply.code(500).send({ error: `Failed to capture snapshot: ${error.message}` });
812
+ }
813
+ });
814
+ // Get steering document
815
+ this.app.get('/api/projects/:projectId/steering/:name', async (request, reply) => {
816
+ const { projectId, name } = request.params;
817
+ const project = this.projectManager.getProject(projectId);
818
+ if (!project) {
819
+ return reply.code(404).send({ error: 'Project not found' });
820
+ }
821
+ const allowedDocs = ['product', 'tech', 'structure'];
822
+ if (!allowedDocs.includes(name)) {
823
+ return reply.code(400).send({ error: 'Invalid steering document name' });
824
+ }
825
+ const docPath = join(project.projectPath, '.spec-workflow', 'steering', `${name}.md`);
826
+ try {
827
+ const content = await readFile(docPath, 'utf-8');
828
+ const stats = await fs.stat(docPath);
829
+ return {
830
+ content,
831
+ lastModified: stats.mtime.toISOString()
832
+ };
833
+ }
834
+ catch {
835
+ return {
836
+ content: '',
837
+ lastModified: new Date().toISOString()
838
+ };
839
+ }
840
+ });
841
+ // Save steering document
842
+ this.app.put('/api/projects/:projectId/steering/:name', async (request, reply) => {
843
+ const { projectId, name } = request.params;
844
+ const { content } = request.body;
845
+ const project = this.projectManager.getProject(projectId);
846
+ if (!project) {
847
+ return reply.code(404).send({ error: 'Project not found' });
848
+ }
849
+ const allowedDocs = ['product', 'tech', 'structure'];
850
+ if (!allowedDocs.includes(name)) {
851
+ return reply.code(400).send({ error: 'Invalid steering document name' });
852
+ }
853
+ if (typeof content !== 'string') {
854
+ return reply.code(400).send({ error: 'Content must be a string' });
855
+ }
856
+ const steeringDir = join(project.projectPath, '.spec-workflow', 'steering');
857
+ const docPath = join(steeringDir, `${name}.md`);
858
+ try {
859
+ await fs.mkdir(steeringDir, { recursive: true });
860
+ await fs.writeFile(docPath, content, 'utf-8');
861
+ return { success: true, message: 'Steering document saved successfully' };
862
+ }
863
+ catch (error) {
864
+ return reply.code(500).send({ error: `Failed to save steering document: ${error.message}` });
865
+ }
866
+ });
867
+ // Get task progress
868
+ this.app.get('/api/projects/:projectId/specs/:name/tasks/progress', async (request, reply) => {
869
+ const { projectId, name } = request.params;
870
+ const project = this.projectManager.getProject(projectId);
871
+ if (!project) {
872
+ return reply.code(404).send({ error: 'Project not found' });
873
+ }
874
+ try {
875
+ const spec = await project.parser.getSpec(name);
876
+ if (!spec || !spec.phases.tasks.exists) {
877
+ return reply.code(404).send({ error: 'Spec or tasks not found' });
878
+ }
879
+ const tasksPath = join(project.projectPath, '.spec-workflow', 'specs', name, 'tasks.md');
880
+ const tasksContent = await readFile(tasksPath, 'utf-8');
881
+ const parseResult = parseTasksFromMarkdown(tasksContent);
882
+ const totalTasks = parseResult.summary.total;
883
+ const completedTasks = parseResult.summary.completed;
884
+ const progress = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
885
+ return {
886
+ total: totalTasks,
887
+ completed: completedTasks,
888
+ inProgress: parseResult.inProgressTask,
889
+ progress: progress,
890
+ taskList: parseResult.tasks,
891
+ lastModified: spec.phases.tasks.lastModified || spec.lastModified
892
+ };
893
+ }
894
+ catch (error) {
895
+ return reply.code(500).send({ error: `Failed to get task progress: ${error.message}` });
896
+ }
897
+ });
898
+ // Update task status
899
+ this.app.put('/api/projects/:projectId/specs/:name/tasks/:taskId/status', async (request, reply) => {
900
+ const { projectId, name, taskId } = request.params;
901
+ const { status } = request.body;
902
+ const project = this.projectManager.getProject(projectId);
903
+ if (!project) {
904
+ return reply.code(404).send({ error: 'Project not found' });
905
+ }
906
+ if (!status || !['pending', 'in-progress', 'completed'].includes(status)) {
907
+ return reply.code(400).send({ error: 'Invalid status. Must be pending, in-progress, or completed' });
908
+ }
909
+ try {
910
+ const tasksPath = join(project.projectPath, '.spec-workflow', 'specs', name, 'tasks.md');
911
+ let tasksContent;
912
+ try {
913
+ tasksContent = await readFile(tasksPath, 'utf-8');
914
+ }
915
+ catch (error) {
916
+ if (error.code === 'ENOENT') {
917
+ return reply.code(404).send({ error: 'Tasks file not found' });
918
+ }
919
+ throw error;
920
+ }
921
+ const parseResult = parseTasksFromMarkdown(tasksContent);
922
+ const task = parseResult.tasks.find(t => t.id === taskId);
923
+ if (!task) {
924
+ return reply.code(404).send({ error: `Task ${taskId} not found` });
925
+ }
926
+ if (task.status === status) {
927
+ return {
928
+ success: true,
929
+ message: `Task ${taskId} already has status ${status}`,
930
+ task: { ...task, status }
931
+ };
932
+ }
933
+ const { updateTaskStatus } = await import('../core/task-parser.js');
934
+ const updatedContent = updateTaskStatus(tasksContent, taskId, status);
935
+ if (updatedContent === tasksContent) {
936
+ return reply.code(500).send({ error: `Failed to update task ${taskId} in markdown content` });
937
+ }
938
+ await fs.writeFile(tasksPath, updatedContent, 'utf-8');
939
+ this.broadcastTaskUpdate(projectId, name);
940
+ return {
941
+ success: true,
942
+ message: `Task ${taskId} status updated to ${status}`,
943
+ task: { ...task, status }
944
+ };
945
+ }
946
+ catch (error) {
947
+ return reply.code(500).send({ error: `Failed to update task status: ${error.message}` });
948
+ }
949
+ });
950
+ // Add implementation log entry
951
+ this.app.post('/api/projects/:projectId/specs/:name/implementation-log', async (request, reply) => {
952
+ const { projectId, name } = request.params;
953
+ const project = this.projectManager.getProject(projectId);
954
+ if (!project) {
955
+ return reply.code(404).send({ error: 'Project not found' });
956
+ }
957
+ try {
958
+ const logData = request.body;
959
+ // Validate artifacts are provided
960
+ if (!logData.artifacts) {
961
+ return reply.code(400).send({ error: 'artifacts field is REQUIRED. Include apiEndpoints, components, functions, classes, or integrations in the artifacts object.' });
962
+ }
963
+ const specPath = join(project.projectPath, '.spec-workflow', 'specs', name);
964
+ const logManager = new ImplementationLogManager(specPath);
965
+ const entry = await logManager.addLogEntry(logData);
966
+ await this.broadcastImplementationLogUpdate(projectId, name);
967
+ return entry;
968
+ }
969
+ catch (error) {
970
+ return reply.code(500).send({ error: `Failed to add implementation log: ${error.message}` });
971
+ }
972
+ });
973
+ // Get implementation logs
974
+ this.app.get('/api/projects/:projectId/specs/:name/implementation-log', async (request, reply) => {
975
+ const { projectId, name } = request.params;
976
+ const query = request.query;
977
+ const project = this.projectManager.getProject(projectId);
978
+ if (!project) {
979
+ return reply.code(404).send({ error: 'Project not found' });
980
+ }
981
+ try {
982
+ const specPath = join(project.projectPath, '.spec-workflow', 'specs', name);
983
+ const logManager = new ImplementationLogManager(specPath);
984
+ let logs = await logManager.getAllLogs();
985
+ if (query.taskId) {
986
+ logs = logs.filter(log => log.taskId === query.taskId);
987
+ }
988
+ if (query.search) {
989
+ logs = await logManager.searchLogs(query.search);
990
+ }
991
+ return { entries: logs };
992
+ }
993
+ catch (error) {
994
+ return reply.code(500).send({ error: `Failed to get implementation logs: ${error.message}` });
995
+ }
996
+ });
997
+ // Get implementation log task stats
998
+ this.app.get('/api/projects/:projectId/specs/:name/implementation-log/task/:taskId/stats', async (request, reply) => {
999
+ const { projectId, name, taskId } = request.params;
1000
+ const project = this.projectManager.getProject(projectId);
1001
+ if (!project) {
1002
+ return reply.code(404).send({ error: 'Project not found' });
1003
+ }
1004
+ try {
1005
+ const specPath = join(project.projectPath, '.spec-workflow', 'specs', name);
1006
+ const logManager = new ImplementationLogManager(specPath);
1007
+ const stats = await logManager.getTaskStats(taskId);
1008
+ return stats;
1009
+ }
1010
+ catch (error) {
1011
+ return reply.code(500).send({ error: `Failed to get implementation log stats: ${error.message}` });
1012
+ }
1013
+ });
1014
+ // Project-specific changelog endpoint
1015
+ this.app.get('/api/projects/:projectId/changelog/:version', async (request, reply) => {
1016
+ const { version } = request.params;
1017
+ try {
1018
+ const changelogPath = join(__dirname, '..', '..', 'CHANGELOG.md');
1019
+ const content = await readFile(changelogPath, 'utf-8');
1020
+ // Extract the section for the requested version
1021
+ const versionRegex = new RegExp(`## \\[${version}\\][^]*?(?=## \\[|$)`, 'i');
1022
+ const match = content.match(versionRegex);
1023
+ if (!match) {
1024
+ return reply.code(404).send({ error: `Changelog for version ${version} not found` });
1025
+ }
1026
+ return { content: match[0].trim() };
1027
+ }
1028
+ catch (error) {
1029
+ if (error.code === 'ENOENT') {
1030
+ return reply.code(404).send({ error: 'Changelog file not found' });
1031
+ }
1032
+ return reply.code(500).send({ error: `Failed to fetch changelog: ${error.message}` });
1033
+ }
1034
+ });
1035
+ // Global changelog endpoint
1036
+ this.app.get('/api/changelog/:version', async (request, reply) => {
1037
+ const { version } = request.params;
1038
+ try {
1039
+ const changelogPath = join(__dirname, '..', '..', 'CHANGELOG.md');
1040
+ const content = await readFile(changelogPath, 'utf-8');
1041
+ // Extract the section for the requested version
1042
+ const versionRegex = new RegExp(`## \\[${version}\\][^]*?(?=## \\[|$)`, 'i');
1043
+ const match = content.match(versionRegex);
1044
+ if (!match) {
1045
+ return reply.code(404).send({ error: `Changelog for version ${version} not found` });
1046
+ }
1047
+ return { content: match[0].trim() };
1048
+ }
1049
+ catch (error) {
1050
+ if (error.code === 'ENOENT') {
1051
+ return reply.code(404).send({ error: 'Changelog file not found' });
1052
+ }
1053
+ return reply.code(500).send({ error: `Failed to fetch changelog: ${error.message}` });
1054
+ }
1055
+ });
1056
+ // Global settings endpoints
1057
+ // Get all automation jobs
1058
+ this.app.get('/api/jobs', async () => {
1059
+ return await this.jobScheduler.getAllJobs();
1060
+ });
1061
+ // Create a new automation job
1062
+ this.app.post('/api/jobs', async (request, reply) => {
1063
+ const job = request.body;
1064
+ if (!job.id || !job.name || !job.type || job.config === undefined || !job.schedule) {
1065
+ return reply.code(400).send({ error: 'Missing required fields: id, name, type, config, schedule' });
1066
+ }
1067
+ try {
1068
+ await this.jobScheduler.addJob({
1069
+ id: job.id,
1070
+ name: job.name,
1071
+ type: job.type,
1072
+ enabled: job.enabled !== false,
1073
+ config: job.config,
1074
+ schedule: job.schedule,
1075
+ createdAt: new Date().toISOString()
1076
+ });
1077
+ return { success: true, message: 'Job created successfully' };
1078
+ }
1079
+ catch (error) {
1080
+ return reply.code(400).send({ error: error.message });
1081
+ }
1082
+ });
1083
+ // Get a specific automation job
1084
+ this.app.get('/api/jobs/:jobId', async (request, reply) => {
1085
+ const { jobId } = request.params;
1086
+ const settingsManager = new (await import('./settings-manager.js')).SettingsManager();
1087
+ try {
1088
+ const job = await settingsManager.getJob(jobId);
1089
+ if (!job) {
1090
+ return reply.code(404).send({ error: 'Job not found' });
1091
+ }
1092
+ return job;
1093
+ }
1094
+ catch (error) {
1095
+ return reply.code(500).send({ error: error.message });
1096
+ }
1097
+ });
1098
+ // Update an automation job
1099
+ this.app.put('/api/jobs/:jobId', async (request, reply) => {
1100
+ const { jobId } = request.params;
1101
+ const updates = request.body;
1102
+ try {
1103
+ await this.jobScheduler.updateJob(jobId, updates);
1104
+ return { success: true, message: 'Job updated successfully' };
1105
+ }
1106
+ catch (error) {
1107
+ return reply.code(400).send({ error: error.message });
1108
+ }
1109
+ });
1110
+ // Delete an automation job
1111
+ this.app.delete('/api/jobs/:jobId', async (request, reply) => {
1112
+ const { jobId } = request.params;
1113
+ try {
1114
+ await this.jobScheduler.deleteJob(jobId);
1115
+ return { success: true, message: 'Job deleted successfully' };
1116
+ }
1117
+ catch (error) {
1118
+ return reply.code(400).send({ error: error.message });
1119
+ }
1120
+ });
1121
+ // Manually run a job
1122
+ this.app.post('/api/jobs/:jobId/run', async (request, reply) => {
1123
+ const { jobId } = request.params;
1124
+ try {
1125
+ const result = await this.jobScheduler.runJobManually(jobId);
1126
+ return result;
1127
+ }
1128
+ catch (error) {
1129
+ return reply.code(400).send({ error: error.message });
1130
+ }
1131
+ });
1132
+ // Get job execution history
1133
+ this.app.get('/api/jobs/:jobId/history', async (request, reply) => {
1134
+ const { jobId } = request.params;
1135
+ const { limit } = request.query;
1136
+ try {
1137
+ const history = await this.jobScheduler.getJobExecutionHistory(jobId, parseInt(limit || '50'));
1138
+ return history;
1139
+ }
1140
+ catch (error) {
1141
+ return reply.code(500).send({ error: error.message });
1142
+ }
1143
+ });
1144
+ // Get job statistics
1145
+ this.app.get('/api/jobs/:jobId/stats', async (request, reply) => {
1146
+ const { jobId } = request.params;
1147
+ try {
1148
+ const stats = await this.jobScheduler.getJobStats(jobId);
1149
+ return stats;
1150
+ }
1151
+ catch (error) {
1152
+ return reply.code(500).send({ error: error.message });
1153
+ }
1154
+ });
1155
+ }
1156
+ broadcastToAll(message) {
1157
+ const messageStr = JSON.stringify(message);
1158
+ this.clients.forEach((connection) => {
1159
+ try {
1160
+ if (connection.socket.readyState === WebSocket.OPEN) {
1161
+ connection.socket.send(messageStr);
1162
+ }
1163
+ }
1164
+ catch (error) {
1165
+ console.error('Error broadcasting to client:', error);
1166
+ this.scheduleConnectionCleanup(connection);
1167
+ }
1168
+ });
1169
+ }
1170
+ broadcastToProject(projectId, message) {
1171
+ const messageStr = JSON.stringify(message);
1172
+ this.clients.forEach((connection) => {
1173
+ try {
1174
+ if (connection.socket.readyState === WebSocket.OPEN && connection.projectId === projectId) {
1175
+ connection.socket.send(messageStr);
1176
+ }
1177
+ }
1178
+ catch (error) {
1179
+ console.error('Error broadcasting to project client:', error);
1180
+ this.scheduleConnectionCleanup(connection);
1181
+ }
1182
+ });
1183
+ }
1184
+ scheduleConnectionCleanup(connection) {
1185
+ // Use setImmediate to avoid modifying Set during iteration
1186
+ setImmediate(() => {
1187
+ try {
1188
+ this.clients.delete(connection);
1189
+ connection.socket.removeAllListeners();
1190
+ if (connection.socket.readyState === WebSocket.OPEN) {
1191
+ connection.socket.close();
1192
+ }
1193
+ }
1194
+ catch {
1195
+ // Ignore cleanup errors
1196
+ }
1197
+ });
1198
+ }
1199
+ startHeartbeat() {
1200
+ this.heartbeatInterval = setInterval(() => {
1201
+ this.clients.forEach((connection) => {
1202
+ if (connection.socket.readyState === WebSocket.OPEN) {
1203
+ try {
1204
+ // Mark as waiting for pong
1205
+ connection.isAlive = false;
1206
+ connection.socket.ping();
1207
+ }
1208
+ catch {
1209
+ this.scheduleConnectionCleanup(connection);
1210
+ }
1211
+ }
1212
+ });
1213
+ // Check for dead connections after timeout
1214
+ setTimeout(() => {
1215
+ this.clients.forEach((connection) => {
1216
+ if (connection.isAlive === false) {
1217
+ console.error('Connection did not respond to heartbeat, cleaning up');
1218
+ this.scheduleConnectionCleanup(connection);
1219
+ }
1220
+ });
1221
+ }, this.HEARTBEAT_TIMEOUT_MS);
1222
+ }, this.HEARTBEAT_INTERVAL_MS);
1223
+ }
1224
+ stopHeartbeat() {
1225
+ if (this.heartbeatInterval) {
1226
+ clearInterval(this.heartbeatInterval);
1227
+ this.heartbeatInterval = undefined;
1228
+ }
1229
+ }
1230
+ async broadcastTaskUpdate(projectId, specName) {
1231
+ try {
1232
+ const project = this.projectManager.getProject(projectId);
1233
+ if (!project)
1234
+ return;
1235
+ const tasksPath = join(project.projectPath, '.spec-workflow', 'specs', specName, 'tasks.md');
1236
+ const tasksContent = await readFile(tasksPath, 'utf-8');
1237
+ const parseResult = parseTasksFromMarkdown(tasksContent);
1238
+ this.broadcastToProject(projectId, {
1239
+ type: 'task-status-update',
1240
+ projectId,
1241
+ data: {
1242
+ specName,
1243
+ taskList: parseResult.tasks,
1244
+ summary: parseResult.summary,
1245
+ inProgress: parseResult.inProgressTask
1246
+ }
1247
+ });
1248
+ }
1249
+ catch (error) {
1250
+ console.error('Error broadcasting task update:', error);
1251
+ }
1252
+ }
1253
+ async broadcastImplementationLogUpdate(projectId, specName) {
1254
+ try {
1255
+ const project = this.projectManager.getProject(projectId);
1256
+ if (!project)
1257
+ return;
1258
+ const specPath = join(project.projectPath, '.spec-workflow', 'specs', specName);
1259
+ const logManager = new ImplementationLogManager(specPath);
1260
+ const logs = await logManager.getAllLogs();
1261
+ this.broadcastToProject(projectId, {
1262
+ type: 'implementation-log-update',
1263
+ projectId,
1264
+ data: {
1265
+ specName,
1266
+ entries: logs
1267
+ }
1268
+ });
1269
+ }
1270
+ catch (error) {
1271
+ console.error('Error broadcasting implementation log update:', error);
1272
+ }
1273
+ }
1274
+ async stop() {
1275
+ // Stop heartbeat monitoring
1276
+ this.stopHeartbeat();
1277
+ // Clear pending spec broadcasts
1278
+ for (const timeout of this.pendingSpecBroadcasts.values()) {
1279
+ clearTimeout(timeout);
1280
+ }
1281
+ this.pendingSpecBroadcasts.clear();
1282
+ // Close all WebSocket connections
1283
+ this.clients.forEach((connection) => {
1284
+ try {
1285
+ connection.socket.removeAllListeners();
1286
+ if (connection.socket.readyState === WebSocket.OPEN) {
1287
+ connection.socket.close();
1288
+ }
1289
+ }
1290
+ catch (error) {
1291
+ // Ignore cleanup errors
1292
+ }
1293
+ });
1294
+ this.clients.clear();
1295
+ // Stop job scheduler
1296
+ await this.jobScheduler.shutdown();
1297
+ // Stop project manager
1298
+ await this.projectManager.stop();
1299
+ // Close the Fastify server
1300
+ await this.app.close();
1301
+ // Unregister from the session manager
1302
+ try {
1303
+ await this.sessionManager.unregisterDashboard();
1304
+ }
1305
+ catch (error) {
1306
+ // Ignore cleanup errors
1307
+ }
1308
+ }
1309
+ getUrl() {
1310
+ return `http://localhost:${this.actualPort}`;
1311
+ }
1312
+ }
1313
+ //# sourceMappingURL=multi-server.js.map