@arimakouyou/spec-workflow-mcp 2.2.7

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