@inharness-ai/claude4spec 1.0.14 → 1.0.16

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 (353) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/bin/c4s/args.d.ts +2 -0
  3. package/dist/bin/c4s/args.js +3 -0
  4. package/dist/bin/c4s/args.js.map +1 -1
  5. package/dist/bin/c4s/commands/ask.js +2 -0
  6. package/dist/bin/c4s/commands/ask.js.map +1 -1
  7. package/dist/bin/c4s/commands/catalog.js +1 -1
  8. package/dist/bin/c4s/commands/catalog.js.map +1 -1
  9. package/dist/bin/c4s/commands/describe.d.ts +2 -0
  10. package/dist/bin/c4s/commands/describe.js +22 -0
  11. package/dist/bin/c4s/commands/describe.js.map +1 -0
  12. package/dist/bin/c4s/commands/find-references.d.ts +13 -0
  13. package/dist/bin/c4s/commands/find-references.js +81 -0
  14. package/dist/bin/c4s/commands/find-references.js.map +1 -0
  15. package/dist/bin/c4s/context.d.ts +4 -3
  16. package/dist/bin/c4s/context.js +17 -6
  17. package/dist/bin/c4s/context.js.map +1 -1
  18. package/dist/bin/c4s/errors.d.ts +1 -1
  19. package/dist/bin/c4s/errors.js.map +1 -1
  20. package/dist/bin/c4s/type-validation.d.ts +3 -0
  21. package/dist/bin/c4s/type-validation.js +14 -0
  22. package/dist/bin/c4s/type-validation.js.map +1 -1
  23. package/dist/bin/c4s-mcp.d.ts +1 -1
  24. package/dist/bin/c4s-mcp.js +23 -12
  25. package/dist/bin/c4s-mcp.js.map +1 -1
  26. package/dist/bin/c4s.js +20 -3
  27. package/dist/bin/c4s.js.map +1 -1
  28. package/dist/bin/claude4spec.js +122 -74
  29. package/dist/bin/claude4spec.js.map +1 -1
  30. package/dist/client/assets/{arc-E0e7X_zn.js → arc-DrA6qHTw.js} +1 -1
  31. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BzlqOVZv.js → architectureDiagram-3BPJPVTR-TEnwTtVX.js} +1 -1
  32. package/dist/client/assets/{blockDiagram-GPEHLZMM-BIQcApN7.js → blockDiagram-GPEHLZMM-txRnQm5w.js} +1 -1
  33. package/dist/client/assets/{c4Diagram-AAUBKEIU-CDU0V1yk.js → c4Diagram-AAUBKEIU-ad8X3Q8n.js} +1 -1
  34. package/dist/client/assets/channel-zmRfuI3H.js +1 -0
  35. package/dist/client/assets/{chunk-2J33WTMH-PtDSwC5f.js → chunk-2J33WTMH-Cp08Xa7f.js} +1 -1
  36. package/dist/client/assets/{chunk-4BX2VUAB-DTUPWrSO.js → chunk-4BX2VUAB-Dc_0On6j.js} +1 -1
  37. package/dist/client/assets/{chunk-55IACEB6-Cpd59xZT.js → chunk-55IACEB6-8d1qQ5z6.js} +1 -1
  38. package/dist/client/assets/{chunk-727SXJPM-C4DOvpJT.js → chunk-727SXJPM-TZenviWx.js} +1 -1
  39. package/dist/client/assets/{chunk-AQP2D5EJ-CtjeHPBB.js → chunk-AQP2D5EJ-DKRRWy-J.js} +1 -1
  40. package/dist/client/assets/{chunk-FMBD7UC4-CZA3OvVr.js → chunk-FMBD7UC4-DLEErJWK.js} +1 -1
  41. package/dist/client/assets/{chunk-ND2GUHAM-Cbx6nQHm.js → chunk-ND2GUHAM-pKGgwz_0.js} +1 -1
  42. package/dist/client/assets/{chunk-QZHKN3VN-3DunELI_.js → chunk-QZHKN3VN-DVcNz8ng.js} +1 -1
  43. package/dist/client/assets/classDiagram-4FO5ZUOK-B9RFgPu-.js +1 -0
  44. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B9RFgPu-.js +1 -0
  45. package/dist/client/assets/{cose-bilkent-S5V4N54A-CKYJnuL7.js → cose-bilkent-S5V4N54A-DZejbxEG.js} +1 -1
  46. package/dist/client/assets/{dagre-BM42HDAG-BrvRwrr_.js → dagre-BM42HDAG-DrPrkFcW.js} +1 -1
  47. package/dist/client/assets/{diagram-2AECGRRQ-BaVLuzcD.js → diagram-2AECGRRQ-Dpn8TJa2.js} +1 -1
  48. package/dist/client/assets/{diagram-5GNKFQAL-9Cu7Wa7J.js → diagram-5GNKFQAL-BxkzEc_t.js} +1 -1
  49. package/dist/client/assets/{diagram-KO2AKTUF-XKiCVdjO.js → diagram-KO2AKTUF-KG0o4LQ7.js} +1 -1
  50. package/dist/client/assets/{diagram-LMA3HP47-DerecU5v.js → diagram-LMA3HP47-CJ-AjQPt.js} +1 -1
  51. package/dist/client/assets/{diagram-OG6HWLK6-BWcREvZ9.js → diagram-OG6HWLK6-D_FG9cDJ.js} +1 -1
  52. package/dist/client/assets/{erDiagram-TEJ5UH35-DU3BBQJY.js → erDiagram-TEJ5UH35-DllhUlzY.js} +1 -1
  53. package/dist/client/assets/{flowDiagram-I6XJVG4X-DXO_IJeR.js → flowDiagram-I6XJVG4X-Bg3OotXu.js} +1 -1
  54. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CXiFZEPx.js → ganttDiagram-6RSMTGT7-BUpUL2Cj.js} +1 -1
  55. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DHewdln-.js → gitGraphDiagram-PVQCEYII-DMRhyWth.js} +1 -1
  56. package/dist/client/assets/{index-DT5g2AtL.js → index-BB2RmhGR.js} +1 -1
  57. package/dist/client/assets/index-BXzOTCgN.css +1 -0
  58. package/dist/client/assets/index-DRFbyGvK.js +788 -0
  59. package/dist/client/assets/{index-DD0GkjkG.js → index-Daewu-VD.js} +1 -1
  60. package/dist/client/assets/{infoDiagram-5YYISTIA-CN40PAGW.js → infoDiagram-5YYISTIA-Bq9D64Ar.js} +1 -1
  61. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-B8KsyuPk.js → ishikawaDiagram-YF4QCWOH-DIaKHHF-.js} +1 -1
  62. package/dist/client/assets/{journeyDiagram-JHISSGLW-BlEb3E_P.js → journeyDiagram-JHISSGLW-gQWGLTeZ.js} +1 -1
  63. package/dist/client/assets/{kanban-definition-UN3LZRKU-B080CGS9.js → kanban-definition-UN3LZRKU-vRgZ5RjE.js} +1 -1
  64. package/dist/client/assets/{linear-D6Tg-IET.js → linear-B4a11szP.js} +1 -1
  65. package/dist/client/assets/{mermaid.core-DP18nW3T.js → mermaid.core-JhUbsJFB.js} +4 -4
  66. package/dist/client/assets/{mindmap-definition-RKZ34NQL-bfo7Qm5I.js → mindmap-definition-RKZ34NQL-ZH-Oz_D-.js} +1 -1
  67. package/dist/client/assets/{pieDiagram-4H26LBE5-ZsPztPu6.js → pieDiagram-4H26LBE5-6rpNrJR6.js} +1 -1
  68. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DKrTleIL.js → quadrantDiagram-W4KKPZXB-DpTKtxEM.js} +1 -1
  69. package/dist/client/assets/{requirementDiagram-4Y6WPE33-D4YX8ZgG.js → requirementDiagram-4Y6WPE33-DpDvZkrK.js} +1 -1
  70. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Bo-xm6Mc.js → sankeyDiagram-5OEKKPKP-KKjWCTYP.js} +1 -1
  71. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CozgdKp6.js → sequenceDiagram-3UESZ5HK-BCmXos1U.js} +1 -1
  72. package/dist/client/assets/{stateDiagram-AJRCARHV-CiK61MwV.js → stateDiagram-AJRCARHV-Xq7jqpMj.js} +1 -1
  73. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C3mDHsxD.js +1 -0
  74. package/dist/client/assets/{timeline-definition-PNZ67QCA-CgHoguI7.js → timeline-definition-PNZ67QCA-CMmq8p5q.js} +1 -1
  75. package/dist/client/assets/{vennDiagram-CIIHVFJN-TY32dxQb.js → vennDiagram-CIIHVFJN-DgK-w2Jv.js} +1 -1
  76. package/dist/client/assets/{wardley-L42UT6IY-C6Q7VFwH.js → wardley-L42UT6IY-C5CwlFHh.js} +1 -1
  77. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-nZLnVsQL.js → wardleyDiagram-YWT4CUSO-D47CtDSc.js} +1 -1
  78. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DHdYRHEr.js → xychartDiagram-2RQKCTM6-C1M_xTVv.js} +1 -1
  79. package/dist/client/index.html +2 -2
  80. package/dist/core/ask/run-ask.d.ts +3 -1
  81. package/dist/core/ask/run-ask.js +75 -54
  82. package/dist/core/ask/run-ask.js.map +1 -1
  83. package/dist/core/references/find-references.d.ts +13 -0
  84. package/dist/core/references/find-references.js +41 -0
  85. package/dist/core/references/find-references.js.map +1 -0
  86. package/dist/core/references/index.d.ts +2 -0
  87. package/dist/core/references/index.js +2 -0
  88. package/dist/core/references/index.js.map +1 -0
  89. package/dist/core/references/types.d.ts +45 -0
  90. package/dist/core/references/types.js +9 -0
  91. package/dist/core/references/types.js.map +1 -0
  92. package/dist/core/workspace/resolve.d.ts +29 -0
  93. package/dist/core/workspace/resolve.js +80 -0
  94. package/dist/core/workspace/resolve.js.map +1 -0
  95. package/dist/server/config.d.ts +58 -6
  96. package/dist/server/config.js +94 -19
  97. package/dist/server/config.js.map +1 -1
  98. package/dist/server/core/plugin-host/cli-engine.d.ts +8 -0
  99. package/dist/server/core/plugin-host/cli-engine.js +16 -0
  100. package/dist/server/core/plugin-host/cli-engine.js.map +1 -0
  101. package/dist/server/core/plugin-host/cross-cutting.js +1 -1
  102. package/dist/server/core/plugin-host/cross-cutting.js.map +1 -1
  103. package/dist/server/core/plugin-host/entities-router.d.ts +3 -1
  104. package/dist/server/core/plugin-host/entities-router.js +24 -19
  105. package/dist/server/core/plugin-host/entities-router.js.map +1 -1
  106. package/dist/server/core/plugin-host/host.js +4 -9
  107. package/dist/server/core/plugin-host/host.js.map +1 -1
  108. package/dist/server/core/plugin-host/project-host.d.ts +38 -0
  109. package/dist/server/core/plugin-host/project-host.js +118 -0
  110. package/dist/server/core/plugin-host/project-host.js.map +1 -0
  111. package/dist/server/core/plugin-host/registry.d.ts +14 -0
  112. package/dist/server/core/plugin-host/registry.js +26 -0
  113. package/dist/server/core/plugin-host/registry.js.map +1 -0
  114. package/dist/server/core/plugin-host/serialization-engine.d.ts +38 -8
  115. package/dist/server/core/plugin-host/serialization-engine.js +63 -39
  116. package/dist/server/core/plugin-host/serialization-engine.js.map +1 -1
  117. package/dist/server/core/plugin-host/types.d.ts +49 -27
  118. package/dist/server/db/index.d.ts +8 -1
  119. package/dist/server/db/index.js +12 -3
  120. package/dist/server/db/index.js.map +1 -1
  121. package/dist/server/db/migrations/035_m29_slug_identity.sql +248 -0
  122. package/dist/server/db/readonly.d.ts +8 -3
  123. package/dist/server/db/readonly.js +7 -4
  124. package/dist/server/db/readonly.js.map +1 -1
  125. package/dist/server/domain/raw-entity-reader.d.ts +5 -5
  126. package/dist/server/domain/raw-entity-reader.js +34 -39
  127. package/dist/server/domain/raw-entity-reader.js.map +1 -1
  128. package/dist/server/entities/ac/ac-analysis.service.d.ts +54 -0
  129. package/dist/server/entities/ac/ac-analysis.service.js +162 -0
  130. package/dist/server/entities/ac/ac-analysis.service.js.map +1 -0
  131. package/dist/server/entities/ac/mcp-server.d.ts +10 -2
  132. package/dist/server/entities/ac/mcp-server.js +34 -3
  133. package/dist/server/entities/ac/mcp-server.js.map +1 -1
  134. package/dist/server/entities/ac/plugin.d.ts +3 -1
  135. package/dist/server/entities/ac/plugin.js +9 -5
  136. package/dist/server/entities/ac/plugin.js.map +1 -1
  137. package/dist/server/entities/ac/serializer.js +1 -1
  138. package/dist/server/entities/ac/serializer.js.map +1 -1
  139. package/dist/server/entities/ac/services.d.ts +19 -7
  140. package/dist/server/entities/ac/services.js +65 -42
  141. package/dist/server/entities/ac/services.js.map +1 -1
  142. package/dist/server/entities/ac/system-prompt.js +1 -8
  143. package/dist/server/entities/ac/system-prompt.js.map +1 -1
  144. package/dist/server/entities/database-table/mcp-server.d.ts +2 -2
  145. package/dist/server/entities/database-table/mcp-server.js +5 -2
  146. package/dist/server/entities/database-table/mcp-server.js.map +1 -1
  147. package/dist/server/entities/database-table/plugin.d.ts +3 -1
  148. package/dist/server/entities/database-table/plugin.js +6 -5
  149. package/dist/server/entities/database-table/plugin.js.map +1 -1
  150. package/dist/server/entities/database-table/routes.d.ts +2 -2
  151. package/dist/server/entities/database-table/serializer.js +1 -1
  152. package/dist/server/entities/database-table/serializer.js.map +1 -1
  153. package/dist/server/entities/database-table/services.d.ts +19 -7
  154. package/dist/server/entities/database-table/services.js +77 -49
  155. package/dist/server/entities/database-table/services.js.map +1 -1
  156. package/dist/server/entities/database-table/system-prompt.js +1 -1
  157. package/dist/server/entities/database-table/system-prompt.js.map +1 -1
  158. package/dist/server/entities/dto/mcp-server.d.ts +2 -2
  159. package/dist/server/entities/dto/mcp-server.js +5 -2
  160. package/dist/server/entities/dto/mcp-server.js.map +1 -1
  161. package/dist/server/entities/dto/plugin.d.ts +3 -1
  162. package/dist/server/entities/dto/plugin.js +6 -5
  163. package/dist/server/entities/dto/plugin.js.map +1 -1
  164. package/dist/server/entities/dto/serializer.js +2 -2
  165. package/dist/server/entities/dto/serializer.js.map +1 -1
  166. package/dist/server/entities/dto/services.d.ts +19 -7
  167. package/dist/server/entities/dto/services.js +73 -49
  168. package/dist/server/entities/dto/services.js.map +1 -1
  169. package/dist/server/entities/dto/system-prompt.js +1 -1
  170. package/dist/server/entities/dto/system-prompt.js.map +1 -1
  171. package/dist/server/entities/endpoint/mcp-server.d.ts +2 -2
  172. package/dist/server/entities/endpoint/mcp-server.js +5 -2
  173. package/dist/server/entities/endpoint/mcp-server.js.map +1 -1
  174. package/dist/server/entities/endpoint/plugin.d.ts +3 -1
  175. package/dist/server/entities/endpoint/plugin.js +6 -5
  176. package/dist/server/entities/endpoint/plugin.js.map +1 -1
  177. package/dist/server/entities/endpoint/serializer.js +4 -4
  178. package/dist/server/entities/endpoint/serializer.js.map +1 -1
  179. package/dist/server/entities/endpoint/services.d.ts +21 -9
  180. package/dist/server/entities/endpoint/services.js +94 -65
  181. package/dist/server/entities/endpoint/services.js.map +1 -1
  182. package/dist/server/entities/endpoint/system-prompt.js +1 -1
  183. package/dist/server/entities/endpoint/system-prompt.js.map +1 -1
  184. package/dist/server/entities/mutate-opts.d.ts +15 -0
  185. package/dist/server/entities/mutate-opts.js +2 -0
  186. package/dist/server/entities/mutate-opts.js.map +1 -0
  187. package/dist/server/entities/ui-view/mcp-server.d.ts +2 -2
  188. package/dist/server/entities/ui-view/mcp-server.js +5 -2
  189. package/dist/server/entities/ui-view/mcp-server.js.map +1 -1
  190. package/dist/server/entities/ui-view/plugin.d.ts +3 -1
  191. package/dist/server/entities/ui-view/plugin.js +6 -5
  192. package/dist/server/entities/ui-view/plugin.js.map +1 -1
  193. package/dist/server/entities/ui-view/routes.d.ts +2 -2
  194. package/dist/server/entities/ui-view/serializer.js +1 -1
  195. package/dist/server/entities/ui-view/serializer.js.map +1 -1
  196. package/dist/server/entities/ui-view/services.d.ts +19 -7
  197. package/dist/server/entities/ui-view/services.js +68 -44
  198. package/dist/server/entities/ui-view/services.js.map +1 -1
  199. package/dist/server/entities/ui-view/system-prompt.d.ts +0 -5
  200. package/dist/server/entities/ui-view/system-prompt.js +3 -7
  201. package/dist/server/entities/ui-view/system-prompt.js.map +1 -1
  202. package/dist/server/external-skills/brief-implementer-template.d.ts +1 -1
  203. package/dist/server/external-skills/brief-implementer-template.js +1 -1
  204. package/dist/server/external-skills/spec-reader-template.d.ts +1 -1
  205. package/dist/server/external-skills/spec-reader-template.js +3 -2
  206. package/dist/server/external-skills/spec-reader-template.js.map +1 -1
  207. package/dist/server/fs/entities-watcher.d.ts +31 -0
  208. package/dist/server/fs/entities-watcher.js +68 -0
  209. package/dist/server/fs/entities-watcher.js.map +1 -0
  210. package/dist/server/fs/watcher.d.ts +2 -2
  211. package/dist/server/fs/watcher.js +7 -1
  212. package/dist/server/fs/watcher.js.map +1 -1
  213. package/dist/server/index.d.ts +11 -1
  214. package/dist/server/index.js +134 -639
  215. package/dist/server/index.js.map +1 -1
  216. package/dist/server/mcp/c4s-reader.d.ts +2 -2
  217. package/dist/server/mcp/c4s-reader.js +41 -4
  218. package/dist/server/mcp/c4s-reader.js.map +1 -1
  219. package/dist/server/mcp/c4s-tools.js +2 -0
  220. package/dist/server/mcp/c4s-tools.js.map +1 -1
  221. package/dist/server/mcp/ensure-mcp-json.d.ts +9 -2
  222. package/dist/server/mcp/ensure-mcp-json.js +18 -4
  223. package/dist/server/mcp/ensure-mcp-json.js.map +1 -1
  224. package/dist/server/mcp/reference-tools.d.ts +8 -2
  225. package/dist/server/mcp/reference-tools.js +52 -179
  226. package/dist/server/mcp/reference-tools.js.map +1 -1
  227. package/dist/server/mcp/release-tools/index.d.ts +2 -2
  228. package/dist/server/routes/agent-turn.d.ts +41 -7
  229. package/dist/server/routes/agent-turn.js +84 -20
  230. package/dist/server/routes/agent-turn.js.map +1 -1
  231. package/dist/server/routes/chat.js +47 -15
  232. package/dist/server/routes/chat.js.map +1 -1
  233. package/dist/server/routes/config.d.ts +24 -0
  234. package/dist/server/routes/config.js +231 -0
  235. package/dist/server/routes/config.js.map +1 -0
  236. package/dist/server/routes/references.d.ts +2 -1
  237. package/dist/server/routes/references.js +4 -5
  238. package/dist/server/routes/references.js.map +1 -1
  239. package/dist/server/routes/releases.d.ts +2 -2
  240. package/dist/server/routes/static.d.ts +11 -0
  241. package/dist/server/routes/static.js +44 -0
  242. package/dist/server/routes/static.js.map +1 -0
  243. package/dist/server/routes/threads.js +14 -2
  244. package/dist/server/routes/threads.js.map +1 -1
  245. package/dist/server/serialization/auto-schema.d.ts +2 -1
  246. package/dist/server/serialization/auto-schema.js +5 -6
  247. package/dist/server/serialization/auto-schema.js.map +1 -1
  248. package/dist/server/serialization/registerAll.d.ts +7 -6
  249. package/dist/server/serialization/registerAll.js +17 -6
  250. package/dist/server/serialization/registerAll.js.map +1 -1
  251. package/dist/server/serialization/resolve-page.d.ts +2 -2
  252. package/dist/server/serialization/serializers/section.d.ts +6 -1
  253. package/dist/server/serialization/serializers/section.js +6 -3
  254. package/dist/server/serialization/serializers/section.js.map +1 -1
  255. package/dist/server/services/brief.d.ts +2 -2
  256. package/dist/server/services/chat-context.d.ts +24 -0
  257. package/dist/server/services/chat-context.js +70 -13
  258. package/dist/server/services/chat-context.js.map +1 -1
  259. package/dist/server/services/entity-indexer.d.ts +42 -0
  260. package/dist/server/services/entity-indexer.js +179 -0
  261. package/dist/server/services/entity-indexer.js.map +1 -0
  262. package/dist/server/services/entity-store.d.ts +68 -0
  263. package/dist/server/services/entity-store.js +162 -0
  264. package/dist/server/services/entity-store.js.map +1 -0
  265. package/dist/server/services/entity-writer.d.ts +12 -1
  266. package/dist/server/services/entity-writer.js +22 -11
  267. package/dist/server/services/entity-writer.js.map +1 -1
  268. package/dist/server/services/git.js +6 -1
  269. package/dist/server/services/git.js.map +1 -1
  270. package/dist/server/services/pages-frontmatter-indexer.d.ts +2 -2
  271. package/dist/server/services/pages-link-indexer.d.ts +2 -2
  272. package/dist/server/services/pages.d.ts +2 -0
  273. package/dist/server/services/pages.js +24 -9
  274. package/dist/server/services/pages.js.map +1 -1
  275. package/dist/server/services/plan.d.ts +2 -2
  276. package/dist/server/services/references.d.ts +25 -0
  277. package/dist/server/services/references.js +102 -21
  278. package/dist/server/services/references.js.map +1 -1
  279. package/dist/server/services/release-bundle.d.ts +0 -1
  280. package/dist/server/services/release-bundle.js +0 -1
  281. package/dist/server/services/release-bundle.js.map +1 -1
  282. package/dist/server/services/release-import.d.ts +2 -0
  283. package/dist/server/services/release-import.js +6 -4
  284. package/dist/server/services/release-import.js.map +1 -1
  285. package/dist/server/services/release.d.ts +10 -10
  286. package/dist/server/services/release.js +32 -63
  287. package/dist/server/services/release.js.map +1 -1
  288. package/dist/server/services/section-indexer.d.ts +4 -3
  289. package/dist/server/services/section-indexer.js +8 -9
  290. package/dist/server/services/section-indexer.js.map +1 -1
  291. package/dist/server/services/skill-registry.d.ts +28 -1
  292. package/dist/server/services/skill-registry.js +51 -10
  293. package/dist/server/services/skill-registry.js.map +1 -1
  294. package/dist/server/services/slug.js +6 -0
  295. package/dist/server/services/slug.js.map +1 -1
  296. package/dist/server/services/static-html.d.ts +24 -0
  297. package/dist/server/services/static-html.js +49 -0
  298. package/dist/server/services/static-html.js.map +1 -0
  299. package/dist/server/services/tags.d.ts +13 -3
  300. package/dist/server/services/tags.js +65 -26
  301. package/dist/server/services/tags.js.map +1 -1
  302. package/dist/server/services/todos-indexer.d.ts +2 -2
  303. package/dist/server/services/versions.d.ts +7 -7
  304. package/dist/server/services/versions.js +34 -37
  305. package/dist/server/services/versions.js.map +1 -1
  306. package/dist/server/workspace/bootstrap.d.ts +42 -0
  307. package/dist/server/workspace/bootstrap.js +73 -0
  308. package/dist/server/workspace/bootstrap.js.map +1 -0
  309. package/dist/server/workspace/context-cache.d.ts +48 -0
  310. package/dist/server/workspace/context-cache.js +147 -0
  311. package/dist/server/workspace/context-cache.js.map +1 -0
  312. package/dist/server/workspace/db-migration.d.ts +11 -0
  313. package/dist/server/workspace/db-migration.js +44 -0
  314. package/dist/server/workspace/db-migration.js.map +1 -0
  315. package/dist/server/workspace/middleware.d.ts +15 -0
  316. package/dist/server/workspace/middleware.js +36 -0
  317. package/dist/server/workspace/middleware.js.map +1 -0
  318. package/dist/server/workspace/project-context.d.ts +65 -0
  319. package/dist/server/workspace/project-context.js +655 -0
  320. package/dist/server/workspace/project-context.js.map +1 -0
  321. package/dist/server/workspace/project-id.d.ts +7 -0
  322. package/dist/server/workspace/project-id.js +11 -0
  323. package/dist/server/workspace/project-id.js.map +1 -0
  324. package/dist/server/workspace/registry.d.ts +58 -0
  325. package/dist/server/workspace/registry.js +264 -0
  326. package/dist/server/workspace/registry.js.map +1 -0
  327. package/dist/server/workspace/routes.d.ts +21 -0
  328. package/dist/server/workspace/routes.js +181 -0
  329. package/dist/server/workspace/routes.js.map +1 -0
  330. package/dist/server/workspace/types.d.ts +30 -0
  331. package/dist/server/workspace/types.js +6 -0
  332. package/dist/server/workspace/types.js.map +1 -0
  333. package/dist/server/ws/gateway.d.ts +12 -2
  334. package/dist/server/ws/gateway.js +51 -9
  335. package/dist/server/ws/gateway.js.map +1 -1
  336. package/dist/server/ws/project-emitter.d.ts +17 -0
  337. package/dist/server/ws/project-emitter.js +12 -0
  338. package/dist/server/ws/project-emitter.js.map +1 -0
  339. package/dist/shared/entities.d.ts +2 -2
  340. package/dist/shared/languages.d.ts +17 -0
  341. package/dist/shared/languages.js +31 -0
  342. package/dist/shared/languages.js.map +1 -0
  343. package/dist/shared/types.d.ts +11 -0
  344. package/dist/shared/xml-tags.d.ts +8 -0
  345. package/dist/shared/xml-tags.js +24 -0
  346. package/dist/shared/xml-tags.js.map +1 -1
  347. package/package.json +10 -3
  348. package/dist/client/assets/channel-BqjILLUr.js +0 -1
  349. package/dist/client/assets/classDiagram-4FO5ZUOK-LjT84-kK.js +0 -1
  350. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-LjT84-kK.js +0 -1
  351. package/dist/client/assets/index-BTXWIk4x.css +0 -1
  352. package/dist/client/assets/index-DcIG_8JB.js +0 -760
  353. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C6-8M5Sb.js +0 -1
@@ -4,64 +4,21 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import fs from 'node:fs';
6
6
  import { createHash } from 'node:crypto';
7
- import { readConfig, writeConfig } from './config.js';
8
- import { openDb } from './db/index.js';
9
- import { PagesService } from './services/pages.js';
10
- import { pagesRouter } from './routes/pages.js';
11
- import { tagsRouter } from './routes/tags.js';
12
- import { entitiesRouter } from './core/plugin-host/entities-router.js';
13
- import { referencesRouter } from './routes/references.js';
14
- import { TagsService, DomainError } from './services/tags.js';
15
- import { VersionService } from './services/versions.js';
16
- import { ReferencesService } from './services/references.js';
17
- import { ChatService } from './services/chat.js';
18
- import { SectionsService } from './services/sections.js';
19
- import { registerExtensionReferenceType } from '../shared/reference-extensions.js';
20
- import { PlanService } from './services/plan.js';
21
- import { plansRouter } from './routes/plans.js';
22
- import { BriefService } from './services/brief.js';
23
- import { briefsRouter } from './routes/briefs.js';
24
- import { PatchService } from './services/patch.js';
25
- import { patchesRouter } from './routes/patches.js';
26
- import { RemoteAuthService } from './services/remote-auth.js';
27
- import { RemoteHttpClient, assertRemoteApiReachable } from './services/remote-http-client.js';
28
- import { remoteAccountRouter } from './routes/remote-account.js';
29
- import { remoteProjectRouter } from './routes/remote-project.js';
30
- import { PagesFrontmatterIndexer } from './services/pages-frontmatter-indexer.js';
31
- import { SectionIndexerService } from './services/section-indexer.js';
32
- import { TodosIndexerService } from './services/todos-indexer.js';
33
- import { PagesLinkIndexerService } from './services/pages-link-indexer.js';
34
- import { PageSerializer } from './services/page-serializer.js';
35
- import { PageVersionService } from './services/page-version.js';
36
- import { RawEntityReader } from './domain/raw-entity-reader.js';
37
- import { ReleaseService } from './services/release.js';
38
- import { releasesRouter } from './routes/releases.js';
39
- import { ReleasePushService } from './services/release-push.js';
40
- import { releasePushesRouter } from './routes/release-pushes.js';
41
- import { ReleaseImportService, rollbackClone } from './services/release-import.js';
42
- import { C4S_VERSION } from './services/release-bundle.js';
43
- import { createReleaseToolsServer } from './mcp/release-tools/index.js';
44
- import { GitService } from './services/git.js';
45
- import { gitRouter } from './routes/git.js';
46
7
  import { WsGateway } from './ws/gateway.js';
47
- import { PagesWatcher } from './fs/watcher.js';
48
- import { createReferenceToolsServer } from './mcp/reference-tools.js';
49
- import { SkillRegistry, SkillResolver, findSkillsDir } from './services/skill-registry.js';
50
- import { chatRouter } from './routes/chat.js';
51
- import { threadsRouter } from './routes/threads.js';
52
- import { sectionsRouter } from './routes/sections.js';
53
- import { todosRouter } from './routes/todos.js';
54
- import { pageLinksRouter } from './routes/page-links.js';
55
- import { errorHandler } from './routes/errors.js';
56
- import { pluginHost } from './core/plugin-host/host.js';
57
- import { pluginHostRouter } from './core/plugin-host/cross-cutting.js';
58
- // Side-effect import: populates pluginHost via legacy adapter for the 4 entity types.
59
- import './serialization/registerAll.js';
8
+ import { WorkspaceRegistry } from './workspace/registry.js';
9
+ import { migrateLegacyDbIfNeeded } from './workspace/db-migration.js';
10
+ import { bootstrapProject } from './workspace/bootstrap.js';
11
+ import { buildProjectContext } from './workspace/project-context.js';
12
+ import { ProjectContextCache } from './workspace/context-cache.js';
13
+ import { projectDispatchMiddleware } from './workspace/middleware.js';
14
+ import { workspaceRouter } from './workspace/routes.js';
15
+ import { PluginRegistryImpl } from './core/plugin-host/registry.js';
16
+ import { registerAllPlugins } from './serialization/registerAll.js';
60
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
61
18
  const DEFAULT_PORT = 3000;
62
19
  // M01: deterministyczny port. Przy zajetym porcie serwer NIE wskakuje juz na
63
20
  // `port+1` — failuje z czytelnym bledem i niezerowym exit code. Powod: stały
64
- // `config.json.port` jest warunkiem discovery serwera przez `c4s ask`.
21
+ // port workspace'u jest warunkiem discovery serwera przez `c4s ask`.
65
22
  async function listenOrExit(server, port) {
66
23
  try {
67
24
  await new Promise((resolve, reject) => {
@@ -81,7 +38,7 @@ async function listenOrExit(server, port) {
81
38
  }
82
39
  catch (err) {
83
40
  if (err.code === 'EADDRINUSE') {
84
- console.error(`port ${port} is in use — stop the other instance or change the port in config.json / pass --port`);
41
+ console.error(`port ${port} is in use — stop the other instance or change the workspace port / pass --port`);
85
42
  process.exit(1);
86
43
  }
87
44
  throw err;
@@ -90,16 +47,41 @@ async function listenOrExit(server, port) {
90
47
  // Wstrzykuje tozsamosc projektu do serwowanego HTML — zaraz po <head>, czyli PRZED
91
48
  // inline theme-scriptem i bundlem React. Klient czyta `window.__C4S_PROJECT__.id`
92
49
  // synchronicznie przy module-load, zeby suffiksowac project-scoped klucze localStorage
93
- // (`::<scope>`). Scope = sha1(cwd) jest niezalezny od portu — kolizja brala sie z reuzycia
94
- // tego samego host:port dla roznych katalogow. Zobacz brief 0.1.400.1.41 (c4sproj01).
95
- function injectProjectGlobal(html, cwd) {
50
+ // (`::<scope>`), zbudowac API_BASE `/api/projects/<id>` i basepath routera `/p/<id>`.
51
+ // M31: cwd przychodzi z ROUTE (`/p/<id>/…`registry lookup), nie z procesu.
52
+ function injectProjectGlobal(html, cwd, name) {
96
53
  const id = createHash('sha1').update(cwd).digest('hex').slice(0, 12);
97
- const payload = JSON.stringify({ id, name: path.basename(cwd) }).replace(/</g, '\\u003c');
54
+ const payload = JSON.stringify({ id, name }).replace(/</g, '\\u003c');
98
55
  return html.replace('<head>', `<head><script>window.__C4S_PROJECT__=${payload};</script>`);
99
56
  }
100
- async function mountDevVite(app, cwd) {
57
+ const PROJECT_ROUTE_RE = /^\/p\/([0-9a-f]{12})(\/|$)/;
58
+ /**
59
+ * M31 route scheme `/p/<project-id>/…` (assets stay at root — no Vite
60
+ * base changes). Decision #11 (0.1.57): `/` → 302 to the last-opened project
61
+ * (else first registered, else `/welcome`); `/welcome` serves the SPA with NO
62
+ * project injected (workspace-scope project list); unknown id and any other
63
+ * non-asset path → redirect `/`.
64
+ */
65
+ function resolveSpaRoute(registry, workspace, urlPath) {
66
+ const m = urlPath.match(PROJECT_ROUTE_RE);
67
+ if (m) {
68
+ const project = registry.getProject(workspace, m[1]);
69
+ return project ? { kind: 'project', project } : { kind: 'redirect', to: '/' };
70
+ }
71
+ if (urlPath === '/welcome')
72
+ return { kind: 'welcome' };
73
+ if (urlPath === '/' || urlPath === '') {
74
+ const fresh = registry.getWorkspace(workspace.name) ?? workspace;
75
+ if (fresh.projects.length === 0)
76
+ return { kind: 'redirect', to: '/welcome' };
77
+ const byRecency = [...fresh.projects].sort((a, b) => (b.lastOpened ?? b.addedAt).localeCompare(a.lastOpened ?? a.addedAt));
78
+ return { kind: 'redirect', to: `/p/${byRecency[0].id}/` };
79
+ }
80
+ return { kind: 'redirect', to: '/' };
81
+ }
82
+ async function mountDevVite(app, deps) {
101
83
  const { createServer } = await import('vite');
102
- const repoRoot = findRepoRoot(cwd);
84
+ const repoRoot = findRepoRoot(deps.startCwd);
103
85
  const vite = await createServer({
104
86
  configFile: path.join(repoRoot, 'vite.config.ts'),
105
87
  root: path.join(repoRoot, 'src/client'),
@@ -111,10 +93,23 @@ async function mountDevVite(app, cwd) {
111
93
  if (req.originalUrl.startsWith('/api') || req.originalUrl.startsWith('/ws'))
112
94
  return next();
113
95
  try {
96
+ // req.path inside app.use('*') is the stripped mount path ('/') — the
97
+ // route lives in originalUrl.
98
+ const urlPath = (req.originalUrl ?? '/').split('?')[0];
99
+ const resolution = resolveSpaRoute(deps.registry, deps.workspace, urlPath);
100
+ if (resolution.kind === 'redirect')
101
+ return res.redirect(302, resolution.to);
114
102
  const htmlPath = path.join(repoRoot, 'src/client/index.html');
115
103
  let html = await fs.promises.readFile(htmlPath, 'utf-8');
116
104
  html = await vite.transformIndexHtml(req.originalUrl, html);
117
- html = injectProjectGlobal(html, cwd);
105
+ // `/welcome` runs the SPA project-less: no `window.__C4S_PROJECT__`, so the
106
+ // client computes PROJECT_ID='' → API_BASE='/api' → router basepath '/'.
107
+ if (resolution.kind === 'welcome') {
108
+ return res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
109
+ }
110
+ const { project } = resolution;
111
+ deps.registry.touchLastOpened(deps.workspace.name, project.id);
112
+ html = injectProjectGlobal(html, project.cwd, project.name);
118
113
  res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
119
114
  }
120
115
  catch (err) {
@@ -124,16 +119,30 @@ async function mountDevVite(app, cwd) {
124
119
  });
125
120
  return async () => { await vite.close(); };
126
121
  }
127
- function mountProd(app, cwd) {
128
- const repoRoot = findRepoRoot(cwd);
122
+ function mountProd(app, deps) {
123
+ const repoRoot = findRepoRoot(deps.startCwd);
129
124
  const clientDist = path.join(repoRoot, 'dist/client');
130
- app.use(express.static(clientDist));
125
+ // index:false — '/' must reach the catch-all (redirect to /p/<id>/), never a raw un-injected index.html.
126
+ app.use(express.static(clientDist, { index: false }));
131
127
  app.use('*', (req, res, next) => {
132
128
  if (req.originalUrl.startsWith('/api') || req.originalUrl.startsWith('/ws'))
133
129
  return next();
134
130
  try {
131
+ const urlPath = (req.originalUrl ?? '/').split('?')[0];
132
+ const resolution = resolveSpaRoute(deps.registry, deps.workspace, urlPath);
133
+ if (resolution.kind === 'redirect')
134
+ return res.redirect(302, resolution.to);
135
135
  const raw = fs.readFileSync(path.join(clientDist, 'index.html'), 'utf-8');
136
- res.status(200).set({ 'Content-Type': 'text/html' }).end(injectProjectGlobal(raw, cwd));
136
+ // `/welcome` runs the SPA project-less (no `window.__C4S_PROJECT__`).
137
+ if (resolution.kind === 'welcome') {
138
+ return res.status(200).set({ 'Content-Type': 'text/html' }).end(raw);
139
+ }
140
+ const { project } = resolution;
141
+ deps.registry.touchLastOpened(deps.workspace.name, project.id);
142
+ res
143
+ .status(200)
144
+ .set({ 'Content-Type': 'text/html' })
145
+ .end(injectProjectGlobal(raw, project.cwd, project.name));
137
146
  }
138
147
  catch (err) {
139
148
  next(err);
@@ -154,595 +163,81 @@ export async function startServer(opts) {
154
163
  const cwd = opts.cwd;
155
164
  const mode = opts.mode ?? (process.env.NODE_ENV === 'production' ? 'prod' : 'dev');
156
165
  const portRef = { current: opts.port ?? DEFAULT_PORT };
157
- // M26 §3: runtime-only ISO timestamp captured at boot; surfaced in
158
- // GET /api/config so the client can detect "Restart required" by comparing
159
- // it to `localStorage['c4s:settings:last-restart-patch-at']`. Not persisted.
160
- const serverStartedAt = new Date().toISOString();
161
166
  const app = express();
162
167
  app.use(express.json({ limit: '2mb' }));
163
- const skillRegistry = SkillRegistry.load(findSkillsDir());
164
- const bootConfig = readConfig(cwd);
165
- // Effective pagesDir precedence: CLI flag > config.json > hardcoded 'pages'.
166
- const pagesDir = opts.pagesDir ?? bootConfig.pagesDir ?? 'pages';
167
- // M21: briefsDir, default '.claude4spec/briefs'. Walidacja: musi byc relative,
168
- // nie moze uciekac z cwd. Kolizja z pagesDir → tylko warn (user moze mocno
169
- // celowo umieszczac brief'y w katalogu pages — frontmatter.type='brief'
170
- // pelni hidden-tree filter).
171
- const briefsDir = bootConfig.briefsDir ?? '.claude4spec/briefs';
172
- if (path.isAbsolute(briefsDir)) {
173
- throw new Error(`config.json: briefsDir must be relative to cwd, got: ${briefsDir}`);
174
- }
175
- const briefsAbs = path.resolve(cwd, briefsDir);
176
- const briefsRel = path.relative(cwd, briefsAbs);
177
- if (briefsRel.startsWith('..') || path.isAbsolute(briefsRel)) {
178
- throw new Error(`config.json: briefsDir must not escape project root, got: ${briefsDir}`);
179
- }
180
- if (briefsDir === pagesDir) {
181
- console.warn(`[config] briefsDir === pagesDir ("${pagesDir}") — brief files will be visible in both indexers`);
182
- }
183
- // M23: patchesDir, default '.claude4spec/patches'. Same validation as briefsDir.
184
- const patchesDir = bootConfig.patchesDir ?? '.claude4spec/patches';
185
- if (path.isAbsolute(patchesDir)) {
186
- throw new Error(`config.json: patchesDir must be relative to cwd, got: ${patchesDir}`);
187
- }
188
- const patchesAbs = path.resolve(cwd, patchesDir);
189
- const patchesRel = path.relative(cwd, patchesAbs);
190
- if (patchesRel.startsWith('..') || path.isAbsolute(patchesRel)) {
191
- throw new Error(`config.json: patchesDir must not escape project root, got: ${patchesDir}`);
192
- }
193
- if (patchesDir === pagesDir || patchesDir === briefsDir) {
194
- console.warn(`[config] patchesDir collides with pagesDir/briefsDir ("${patchesDir}") — patch files will be visible in multiple indexers`);
195
- }
196
- // M01 (0.1.36): resolve the remote base URL with precedence
197
- // `--remote-url` flag (opts) > config.json > prod constant. The prod-constant
198
- // fallback lives in RemoteHttpClient; here `null` means "use prod".
199
- const remoteApiUrl = opts.remoteApiUrl ?? bootConfig.remoteApiUrl;
200
- // M24: an explicit remoteApiUrl override (flag or config.json) must be a valid,
201
- // reachable host — hard error at boot, no fallback to the production constant.
202
- if (remoteApiUrl != null && remoteApiUrl.trim() !== '') {
203
- await assertRemoteApiReachable(remoteApiUrl);
204
- }
205
- pluginHost.consolidate(bootConfig.entities);
206
- const hostState = pluginHost.state();
207
- console.log(`[plugin-host] active: [${hostState.active.join(', ') || '∅'}]` +
208
- (hostState.inactive.length ? `, inactive: [${hostState.inactive.join(', ')}]` : '') +
209
- (hostState.unknown.length ? `, unknown: [${hostState.unknown.join(', ')}]` : ''));
210
- const initialWritingStyle = bootConfig.writingStyle;
211
- if (initialWritingStyle !== null && !skillRegistry.isSelectable(initialWritingStyle)) {
212
- const available = skillRegistry.listSelectable().map((s) => s.slug).join(', ') || '(none)';
213
- throw new Error(`config.json: writingStyle "${initialWritingStyle}" not a selectable writing-style skill. Available: ${available}`);
214
- }
215
- const skillResolver = new SkillResolver(skillRegistry, cwd);
216
- const db = openDb(cwd);
217
- const pages = new PagesService(cwd, pagesDir);
218
- await pages.ensureRoot();
219
- // M21 m02multidir: drugi PagesService dla briefsDir. Wspoldziel z pierwszym
220
- // zarowno PageVersionService jak i WsGateway, ale ma osobny watcher (debounce
221
- // independent per katalog) i osobny PageSerializer (constructor-bound do
222
- // odpowiedniego rootDir).
223
- const briefsPages = new PagesService(cwd, briefsDir);
224
- await briefsPages.ensureRoot();
225
- // M23 m02multidir: trzeci PagesService dla patchesDir (analogicznie do briefsDir).
226
- const patchesPages = new PagesService(cwd, patchesDir);
227
- await patchesPages.ensureRoot();
228
- const tagsService = new TagsService(db.handle);
229
- const versionService = new VersionService(db.handle);
230
- const rawReader = new RawEntityReader(db.handle);
231
- // M17: wire snapshot capture deps. After this, every entity service
232
- // mutation captures a deterministic snapshot via host.snapshot(...).
233
- versionService.configureSnapshot(rawReader, pluginHost);
234
- // M24: remote-account identity (device flow + local session). Single HTTP
235
- // client per process; base URL from config.remoteApiUrl (or the prod constant).
236
- const remoteHttpClient = new RemoteHttpClient(remoteApiUrl);
237
- const remoteAuthService = new RemoteAuthService(db.handle, remoteHttpClient);
238
- const chatService = new ChatService(db.handle);
239
- // Orphan cleanup: rowsy chat_message.status='streaming' pozostale po crashu poprzedniego
240
- // procesu (SIGKILL/OOM) — brak aktywnego adaptera po starcie, flipujemy wszystkie na 'complete'.
241
- chatService.finalizeAllStreamingRows();
242
- app.get('/api/health', (_req, res) => {
243
- res.json({ ok: true, mode, cwd });
244
- });
245
- app.get('/api/meta', (_req, res) => {
246
- res.json({ cwd, cwdName: path.basename(cwd), c4sVersion: C4S_VERSION });
247
- });
248
- app.get('/api/config', (_req, res) => {
249
- // readConfig per-request: PATCH /api/config musi byc widoczny w GET bez restartu.
250
- // Spojne z istniejacym wzorcem SkillResolver (per-query disk read).
251
- const c = readConfig(cwd);
252
- res.json({
253
- name: c.name,
254
- port: portRef.current,
255
- pagesDir: c.pagesDir,
256
- mode,
257
- writingStyle: c.writingStyle,
258
- onboarding: { completed: c.onboardingCompleted },
259
- briefsDir: c.briefsDir,
260
- patchesDir: c.patchesDir,
261
- entities: c.entities,
262
- agent: { claudeUsePreset: c.agent?.claudeUsePreset ?? true },
263
- git: {
264
- syncCommitOnRelease: c.git?.syncCommitOnRelease ?? false,
265
- syncPushOnPush: c.git?.syncPushOnPush ?? false,
266
- },
267
- remoteProjectId: c.remoteProjectId ?? null,
268
- remoteApiUrl: c.remoteApiUrl ?? null,
269
- $schemaVersion: c.$schemaVersion,
270
- serverStartedAt,
271
- });
272
- });
273
- app.patch('/api/config', (req, res, next) => {
274
- try {
275
- const body = (req.body ?? {});
276
- // M26 §2: hot-reload (name, writingStyle, agent.claudeUsePreset) and
277
- // restart-required (port, mode, pagesDir, briefsDir, patchesDir, entities)
278
- // share one atomic disk write. `writeConfig` re-runs the full validation in
279
- // `config.ts`; this handler only enforces semantic checks the validator
280
- // cannot do (writingStyle selectability, name regex, path safety).
281
- const patch = {};
282
- if ('name' in body) {
283
- if (typeof body.name !== 'string') {
284
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'name must be a string' } });
285
- }
286
- const trimmed = body.name.trim();
287
- if (trimmed.length < 1 || trimmed.length > 80 || !/^[a-zA-Z0-9._\- ]+$/.test(trimmed)) {
288
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'name: 1-80 chars, allowed [a-zA-Z0-9._- ]' } });
289
- }
290
- patch.name = trimmed;
291
- }
292
- if ('port' in body) {
293
- if (typeof body.port !== 'number' || !Number.isInteger(body.port) || body.port < 1 || body.port > 65535) {
294
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'port must be an integer in [1, 65535]' } });
295
- }
296
- patch.port = body.port;
297
- }
298
- if ('mode' in body) {
299
- if (body.mode !== 'dev' && body.mode !== 'prod') {
300
- return res.status(400).json({ error: { code: 'VALIDATION', message: "mode must be 'dev' | 'prod'" } });
301
- }
302
- patch.mode = body.mode;
303
- }
304
- // pagesDir/briefsDir/patchesDir share the same path-safety contract as boot:
305
- // must be relative, must not escape cwd. Reuse the local helper inline to
306
- // keep validation co-located with the patch handler.
307
- const validateDir = (field, value) => {
308
- if (typeof value !== 'string' || value.trim() === '') {
309
- return { error: `${field} must be a non-empty string` };
310
- }
311
- if (path.isAbsolute(value)) {
312
- return { error: `${field} must be relative to cwd` };
313
- }
314
- const abs = path.resolve(cwd, value);
315
- const rel = path.relative(cwd, abs);
316
- if (rel.startsWith('..') || path.isAbsolute(rel)) {
317
- return { error: `${field} must not escape project root` };
318
- }
319
- return value;
320
- };
321
- for (const field of ['pagesDir', 'briefsDir', 'patchesDir']) {
322
- if (field in body) {
323
- const result = validateDir(field, body[field]);
324
- if (typeof result === 'object') {
325
- return res.status(400).json({ error: { code: 'VALIDATION', message: result.error } });
326
- }
327
- patch[field] = result;
328
- }
329
- }
330
- if ('writingStyle' in body) {
331
- if (body.writingStyle !== null && typeof body.writingStyle !== 'string') {
332
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'writingStyle must be string | null' } });
333
- }
334
- if (typeof body.writingStyle === 'string' && !skillRegistry.isSelectable(body.writingStyle)) {
335
- const available = skillRegistry.listSelectable().map((s) => s.slug).join(', ') || '(none)';
336
- return res.status(400).json({ error: { code: 'VALIDATION', message: `writingStyle "${body.writingStyle}" not a selectable writing-style skill. Available: ${available}` } });
337
- }
338
- patch.writingStyle = body.writingStyle;
339
- }
340
- if ('onboardingCompleted' in body) {
341
- if (typeof body.onboardingCompleted !== 'boolean') {
342
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'onboardingCompleted must be boolean' } });
343
- }
344
- patch.onboardingCompleted = body.onboardingCompleted;
345
- }
346
- if ('entities' in body) {
347
- if (!Array.isArray(body.entities) || !body.entities.every((e) => typeof e === 'string')) {
348
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'entities must be string[]' } });
349
- }
350
- patch.entities = body.entities;
351
- }
352
- if ('agent' in body) {
353
- const a = body.agent;
354
- if (a === null || typeof a !== 'object' || Array.isArray(a)) {
355
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'agent must be an object' } });
356
- }
357
- const ar = a;
358
- const next = {};
359
- if ('claudeUsePreset' in ar) {
360
- if (typeof ar.claudeUsePreset !== 'boolean') {
361
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'agent.claudeUsePreset must be boolean' } });
362
- }
363
- next.claudeUsePreset = ar.claudeUsePreset;
364
- }
365
- patch.agent = next;
366
- }
367
- // M28: hot-reload git-sync toggles. Only present subfields are forwarded;
368
- // writeConfig deep-merges `git` so the untouched toggle is preserved.
369
- if ('git' in body) {
370
- const g = body.git;
371
- if (g === null || typeof g !== 'object' || Array.isArray(g)) {
372
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'git must be an object' } });
373
- }
374
- const gr = g;
375
- const next = {};
376
- if ('syncCommitOnRelease' in gr) {
377
- if (typeof gr.syncCommitOnRelease !== 'boolean') {
378
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'git.syncCommitOnRelease must be boolean' } });
379
- }
380
- next.syncCommitOnRelease = gr.syncCommitOnRelease;
381
- }
382
- if ('syncPushOnPush' in gr) {
383
- if (typeof gr.syncPushOnPush !== 'boolean') {
384
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'git.syncPushOnPush must be boolean' } });
385
- }
386
- next.syncPushOnPush = gr.syncPushOnPush;
387
- }
388
- patch.git = next;
389
- }
390
- // M25: allow manual clear/override of remoteProjectId (e.g. UI "clear" after
391
- // a stale UUID). null ⇒ next push is a first push again.
392
- if ('remoteProjectId' in body) {
393
- if (body.remoteProjectId !== null && typeof body.remoteProjectId !== 'string') {
394
- return res.status(400).json({ error: { code: 'VALIDATION', message: 'remoteProjectId must be string | null' } });
395
- }
396
- patch.remoteProjectId = body.remoteProjectId;
397
- }
398
- const updated = writeConfig(cwd, patch);
399
- res.json({
400
- name: updated.name,
401
- port: portRef.current,
402
- pagesDir: updated.pagesDir,
403
- mode,
404
- writingStyle: updated.writingStyle,
405
- onboarding: { completed: updated.onboardingCompleted },
406
- briefsDir: updated.briefsDir,
407
- patchesDir: updated.patchesDir,
408
- entities: updated.entities,
409
- agent: { claudeUsePreset: updated.agent?.claudeUsePreset ?? true },
410
- git: {
411
- syncCommitOnRelease: updated.git?.syncCommitOnRelease ?? false,
412
- syncPushOnPush: updated.git?.syncPushOnPush ?? false,
413
- },
414
- remoteProjectId: updated.remoteProjectId ?? null,
415
- remoteApiUrl: updated.remoteApiUrl ?? null,
416
- $schemaVersion: updated.$schemaVersion,
417
- serverStartedAt,
418
- });
419
- }
420
- catch (err) {
421
- next(err);
422
- }
423
- });
424
- app.get('/api/writing-styles', (_req, res) => {
425
- const c = readConfig(cwd);
426
- res.json({
427
- active: c.writingStyle,
428
- available: skillRegistry.listSelectable().map((s) => ({
429
- slug: s.slug,
430
- title: s.title,
431
- description: s.description,
432
- version: s.version,
433
- language: s.language,
434
- })),
435
- });
436
- });
168
+ // M31: workspace registry — DB lives in the workspace slot, not the project dir.
169
+ const registry = new WorkspaceRegistry();
170
+ const workspace = opts.workspace ?? registry.selectOrCreate({ port: opts.port, mode: opts.mode });
171
+ // Decision #11: a workspace-only start (`createProject === false`) registers
172
+ // and activates nothing `cwd` stays untouched and the bare command lands on
173
+ // `/welcome`. Default `true` keeps the historic "always create" behavior.
174
+ const createProject = opts.createProject ?? true;
175
+ const initialProject = createProject ? registry.registerProject(workspace, cwd) : null;
176
+ if (createProject)
177
+ migrateLegacyDbIfNeeded(registry, workspace, cwd);
178
+ // M31: process-immutable plugin catalog, populated exactly once.
179
+ const pluginRegistry = new PluginRegistryImpl();
180
+ registerAllPlugins(pluginRegistry);
437
181
  const httpServer = createHttpServer(app);
438
182
  const gateway = new WsGateway(httpServer);
439
- const watcher = new PagesWatcher(pages.root, gateway);
440
- // M21 m02multidir: drugi PagesWatcher na briefsDir. Wspoldzieli ten sam
441
- // gateway (broadcast `page:changed` z drugiego katalogu — UI może slot-detect
442
- // przez prefix sciezki, jezeli kiedys bedzie potrzebne).
443
- const briefsWatcher = new PagesWatcher(briefsPages.root, gateway);
444
- // M23 m02multidir: trzeci PagesWatcher na patchesDir.
445
- const patchesWatcher = new PagesWatcher(patchesPages.root, gateway);
446
- const referencesService = new ReferencesService(pages, watcher);
447
- const sectionsService = new SectionsService(db.handle);
448
- sectionsService.setWriteDeps({ pages, watcher });
449
- // M06 registers <section_ref/> as the 6th XML reference type via the
450
- // M19 extension reference types slot. Parser/serializer for the 5 core
451
- // types in xml-tags.ts is untouched; the slot lives in reference-extensions.ts.
452
- registerExtensionReferenceType({
453
- tag: 'section_ref',
454
- attrOrder: ['anchor'],
455
- validate: (attrs) => {
456
- const anchor = attrs.anchor ?? '';
457
- return anchor && sectionsService.has(anchor)
458
- ? { ok: true, category: 'ok' }
459
- : { ok: false, category: 'unknown-anchor' };
460
- },
461
- });
462
- const planService = new PlanService(db.handle, gateway, chatService);
463
- const sectionIndexer = new SectionIndexerService(db.handle, pages, watcher, gateway);
464
- const todosIndexer = new TodosIndexerService(pages, gateway);
465
- const pagesLinkIndexer = new PagesLinkIndexerService(pages, gateway);
466
- // M17: page versioning (out of L9 plugin host — decyzja 1)
467
- const pageSerializer = new PageSerializer(pages);
468
- // M21 m02multidir: osobny serializer dla briefsDir (PageSerializer trzyma
469
- // referencje do PagesService w konstruktorze — czyta z dysku przez nia).
470
- // PageVersionService akceptuje override serializera w recordVersion(),
471
- // wiec dwa serializery dziela jedna instancje wersjonowania.
472
- const briefsSerializer = new PageSerializer(briefsPages);
473
- // M23: serializer dla patchesDir (analogicznie do briefsSerializer).
474
- const patchesSerializer = new PageSerializer(patchesPages);
475
- const pageVersions = new PageVersionService(db.handle, pageSerializer);
476
- // M21 m02fmidx / M23: in-memory frontmatter indexer feeded przez trzy watchery.
477
- const pagesFrontmatterIndexer = new PagesFrontmatterIndexer(pages, briefsPages, patchesPages, gateway);
478
- // Mount all active backend modules — each plugin constructs its own service,
479
- // mounts its router, registers its MCP server, and wires its id resolver via
480
- // the supplied MountContext. Inactive plugins are skipped (config.entities).
481
- pluginHost.mountBackend({
482
- app,
483
- db: db.handle,
484
- ws: gateway,
485
- tagsService,
486
- versionService,
487
- referencesService,
488
- registerMcpServer: (name, server) => pluginHost.registerMcpServer(name, server),
489
- setIdResolver: (type, fn) => pluginHost.setIdResolver(type, fn),
490
- registerEntityService: (type, service) => pluginHost.registerEntityService(type, service),
491
- });
492
- // Cross-cutting MCP server — owned by the host, not a plugin (M13).
493
- // Registered as a factory: a fresh instance is built per agent turn so
494
- // concurrent turns never share one MCP transport.
495
- pluginHost.registerMcpServer('reference-tools', () => createReferenceToolsServer({
496
- tagsService,
497
- referencesService,
498
- pagesService: pages,
499
- sectionsService,
500
- ws: gateway,
501
- db: db.handle,
502
- cwd,
503
- }));
504
- // M17: ReleaseService + cross-cutting `release-tools` MCP. Like
505
- // reference-tools, owned by the host (not a plugin) — release semantics
506
- // are dual-track (entities + pages), neither side is a plugin owner.
507
- const releaseService = new ReleaseService(db.handle, pluginHost, versionService, pageVersions, pageSerializer, rawReader, tagsService, pages, watcher, cwd);
508
- // M28 Git Sync — best-effort mirroring of release create/push into the user's
509
- // git repo. Probes `pages.root` for the worktree; reads config per-action.
510
- const gitService = new GitService(cwd, pages.root);
511
- // M25 Release Push — coordinates M17 bundle build + M24 transport; owns release_push.
512
- const releasePushService = new ReleasePushService(db.handle, releaseService, remoteAuthService, gitService, cwd);
513
- pluginHost.registerMcpServer('release-tools', () => createReleaseToolsServer({ releaseService, gitService, ws: gateway }));
514
- // M27 Project Clone — bootstrap-time only. Runs after services exist (DB
515
- // migrated, plugin host mounted, pages root ensured) but BEFORE watchers start
516
- // and before app.listen, so restore writes land without watcher double-capture.
517
- if (opts.clone) {
518
- const importService = new ReleaseImportService(db.handle, releaseService, remoteHttpClient, cwd);
519
- try {
520
- const result = await importService.clone(opts.clone, { nameOverride: opts.name });
521
- console.log(` cloned remote project '${opts.clone}' → local release #${result.localReleaseId ?? '?'}`);
522
- }
523
- catch (err) {
524
- const code = err instanceof DomainError ? err.code : 'CLONE_FAILED';
525
- console.error(`\x1b[31mclone failed\x1b[0m: ${code} — ${err instanceof Error ? err.message : String(err)}`);
526
- // Full all-or-nothing rollback: cwd returns to its pre-`--clone` state. Close
527
- // the DB handle first so db.sqlite can be unlinked (required on Windows).
528
- db.close();
529
- rollbackClone(cwd, {
530
- pagesDir,
531
- configCreated: opts.configCreated ?? false,
532
- claudeDirCreated: opts.claudeDirCreated ?? false,
533
- gitignoreCreated: opts.gitignoreCreated ?? false,
534
- });
535
- process.exit(1);
536
- }
537
- }
538
- // M21: BriefService — top-level (nie plugin), wzorzec analogiczny do
539
- // PlanService. Mountowany router /api/briefs poniżej.
540
- const briefService = new BriefService({
541
- briefsPages,
542
- briefsWatcher,
543
- briefsSerializer,
544
- pageVersions,
545
- chatService,
546
- releaseService,
547
- frontmatterIndexer: pagesFrontmatterIndexer,
548
- ws: gateway,
549
- });
550
- // M23: PatchService — top-level (nie plugin), wzorzec analogiczny do
551
- // BriefService. Mountowany router /api/patches poniżej.
552
- const patchService = new PatchService({
553
- patchesPages,
554
- patchesWatcher,
555
- patchesSerializer,
556
- pageVersions,
557
- chatService,
558
- frontmatterIndexer: pagesFrontmatterIndexer,
183
+ // M27 clone runs only on the FIRST build of the CLI-started project —
184
+ // consumed here so a later cache rebuild never re-clones.
185
+ let clonePending = opts.clone
186
+ ? {
187
+ slug: opts.clone,
188
+ nameOverride: opts.name,
189
+ configCreated: opts.configCreated ?? false,
190
+ claudeDirCreated: opts.claudeDirCreated ?? false,
191
+ gitignoreCreated: opts.gitignoreCreated ?? false,
192
+ }
193
+ : undefined;
194
+ // M31: lazily-built, cached per-project contexts. The CLI-only overrides
195
+ // (pagesDir flag, --remote-url, clone) apply solely to the initial project.
196
+ const cache = new ProjectContextCache(async (project) => {
197
+ const isInitial = initialProject != null && project.id === initialProject.id;
198
+ const clone = isInitial ? clonePending : undefined;
199
+ if (clone)
200
+ clonePending = undefined;
201
+ return buildProjectContext({
202
+ registry,
203
+ pluginRegistry,
204
+ workspace,
205
+ cwd: project.cwd,
206
+ gateway,
207
+ mode,
208
+ remoteApiUrl: isInitial ? opts.remoteApiUrl : undefined,
209
+ pagesDirOverride: isInitial ? opts.pagesDir : undefined,
210
+ clone,
211
+ onTurnFinished: () => cache.reapIdle(),
212
+ onContextConfigChanged: () => cache.invalidate(project.id),
213
+ });
559
214
  });
560
- app.use('/api', pluginHostRouter(pluginHost));
561
- app.use('/api/pages', pagesRouter(pages, watcher, pageVersions));
562
- app.use('/api/tags', tagsRouter(tagsService, referencesService));
563
- app.use('/api/references', referencesRouter(referencesService));
564
- app.use('/api/entities', entitiesRouter(tagsService, versionService));
565
- // Wspolne deps tury agenta — `threadsRouter` (POST /:id/ask) i `chatRouter`
566
- // (POST /api/chat, SSE) dziela ten sam runtime i rejestr `activeAdapters`.
567
- const agentDeps = {
568
- chatService,
569
- pagesService: pages,
570
- tagsService,
571
- sectionsService,
572
- planService,
573
- briefService,
574
- patchService,
575
- pageVersions,
576
- skillResolver,
577
- skillRegistry,
578
- ws: gateway,
579
- cwd,
580
- pagesDir,
581
- mode,
582
- db,
215
+ // Per-project activation — POST /api/workspace/projects runs the SAME full
216
+ // bootstrap as a CLI start (M01/M12/M22 hooks + registration + db migration).
217
+ const activateProject = async (projectCwd) => {
218
+ return bootstrapProject(registry, workspace, projectCwd).project;
583
219
  };
584
- app.use('/api/threads', threadsRouter(agentDeps));
585
- app.use('/api/sections', sectionsRouter(sectionsService));
586
- app.use('/api/todos', todosRouter(todosIndexer));
587
- app.use('/api/page-links', pageLinksRouter(pagesLinkIndexer));
588
- app.use('/api/plans', plansRouter(planService));
589
- app.use('/api/releases', releasesRouter(releaseService, gateway, gitService));
590
- app.use('/api/release-pushes', releasePushesRouter(releasePushService));
591
- app.use('/api/git', gitRouter(gitService));
592
- app.use('/api/briefs', briefsRouter(briefService, pageVersions));
593
- app.use('/api/patches', patchesRouter(patchService));
594
- app.use('/api/remote-account', remoteAccountRouter(remoteAuthService));
595
- app.use('/api/remote-project', remoteProjectRouter(remoteAuthService, cwd));
596
- app.use('/api/chat', chatRouter(agentDeps));
597
- app.use(errorHandler);
598
- watcher.onChange((relPath, kind) => {
599
- if (kind === 'unlink') {
600
- sectionIndexer.handleUnlink(relPath).catch((err) => {
601
- console.error('[section-indexer] unlink error:', err);
602
- });
603
- todosIndexer.handleUnlink(relPath);
604
- pagesLinkIndexer.handleUnlink(relPath);
605
- pagesFrontmatterIndexer.handleUnlink('pages', relPath);
606
- // M17: capture filesystem-origin delete (chokidar saw external rm)
607
- pageVersions.recordVersion(relPath, 'delete', 'filesystem').catch((err) => {
608
- console.warn(`[page-version] watcher delete capture for ${relPath}:`, err.message);
609
- });
610
- }
611
- else {
612
- sectionIndexer.schedulePage(relPath);
613
- todosIndexer.schedulePage(relPath);
614
- pagesLinkIndexer.schedulePage(relPath);
615
- pagesFrontmatterIndexer.schedulePage('pages', relPath);
616
- // M17: capture filesystem-origin add/change. `kind === 'add'` may be a
617
- // real new file (op=create) or a re-detection — pageVersions.hasAny
618
- // distinguishes.
619
- const op = kind === 'add' && !pageVersions.hasAny(relPath) ? 'create' : 'update';
620
- pageVersions.recordVersion(relPath, op, 'filesystem').catch((err) => {
621
- console.warn(`[page-version] watcher capture for ${relPath}:`, err.message);
622
- });
623
- }
624
- });
625
- // M21 m02multidir: drugi watcher dla briefsDir. Tylko frontmatter indexer
626
- // + page_version (z dedykowanym briefsSerializer). Section/todos/pages-link
627
- // indexery NIE pracuja na briefsDir (briefs to nie pages w sensie M02 →
628
- // nie czesc nawigowalnego drzewa, nie agreguja section_ref/todo'ow do tabel).
629
- briefsWatcher.onChange((relPath, kind) => {
630
- if (kind === 'unlink') {
631
- pagesFrontmatterIndexer.handleUnlink('briefs', relPath);
632
- pageVersions.recordVersion(relPath, 'delete', 'filesystem', undefined, briefsSerializer, 'brief').catch((err) => {
633
- console.warn(`[page-version] brief delete capture for ${relPath}:`, err.message);
634
- });
635
- }
636
- else {
637
- pagesFrontmatterIndexer.schedulePage('briefs', relPath);
638
- const op = kind === 'add' && !pageVersions.hasAny(relPath) ? 'create' : 'update';
639
- pageVersions.recordVersion(relPath, op, 'filesystem', undefined, briefsSerializer, 'brief').catch((err) => {
640
- console.warn(`[page-version] brief capture for ${relPath}:`, err.message);
641
- });
642
- // Direct disk edit (not suppressed): refresh open BriefEditors. The indexer
643
- // only fires `briefs:changed` on frontmatter changes, so body-only edits
644
- // need this explicit broadcast. `external` → reload-or-confirm like Pages.
645
- gateway.broadcast({ kind: 'briefs:changed', path: relPath, origin: 'external' });
646
- }
647
- });
648
- // M23: trzeci watcher dla patchesDir — analogicznie do briefsWatcher.
649
- patchesWatcher.onChange((relPath, kind) => {
650
- if (kind === 'unlink') {
651
- pagesFrontmatterIndexer.handleUnlink('patches', relPath);
652
- pageVersions.recordVersion(relPath, 'delete', 'filesystem', undefined, patchesSerializer, 'patch').catch((err) => {
653
- console.warn(`[page-version] patch delete capture for ${relPath}:`, err.message);
654
- });
655
- }
656
- else {
657
- pagesFrontmatterIndexer.schedulePage('patches', relPath);
658
- const op = kind === 'add' && !pageVersions.hasAny(relPath) ? 'create' : 'update';
659
- pageVersions.recordVersion(relPath, op, 'filesystem', undefined, patchesSerializer, 'patch').catch((err) => {
660
- console.warn(`[page-version] patch capture for ${relPath}:`, err.message);
661
- });
662
- }
663
- });
664
- watcher.start();
665
- briefsWatcher.start();
666
- patchesWatcher.start();
667
- sectionIndexer.indexAll().catch((err) => {
668
- console.error('[section-indexer] initial indexAll failed:', err);
669
- });
670
- todosIndexer.indexAll().catch((err) => {
671
- console.error('[todos-indexer] initial indexAll failed:', err);
672
- });
673
- pagesLinkIndexer.indexAll().catch((err) => {
674
- console.error('[pages-link-indexer] initial indexAll failed:', err);
675
- });
676
- // M17: initial sync of page_version. For each markdown file with no captured
677
- // version — or whose latest captured version is a `delete` tombstone while the
678
- // file is back on disk — write an `op = 'create'` baseline. The latter case
679
- // covers delete+recreate that happened while the server wasn't watching
680
- // (server down, `git checkout` between restarts): without this, the phantom
681
- // `delete` stays the latest row and release diffs show the page as removed.
682
- (async () => {
683
- try {
684
- const files = await pages.listMarkdownFiles();
685
- for (const relPath of files) {
686
- const latest = pageVersions.getLatestForPath(relPath);
687
- if (latest && latest.op !== 'delete')
688
- continue;
689
- await pageVersions.recordVersion(relPath, 'create', 'filesystem');
690
- }
691
- }
692
- catch (err) {
693
- console.warn('[page-version] initial sync failed:', err.message);
694
- }
695
- })();
696
- // M21: initial sync — page_version baseline dla briefów + frontmatter indexer.
697
- (async () => {
698
- try {
699
- const files = await briefsPages.listMarkdownFiles();
700
- for (const relPath of files) {
701
- if (pageVersions.hasAny(relPath))
702
- continue;
703
- await pageVersions.recordVersion(relPath, 'create', 'filesystem', undefined, briefsSerializer, 'brief');
704
- }
705
- }
706
- catch (err) {
707
- console.warn('[page-version] briefs initial sync failed:', err.message);
708
- }
709
- // M23: initial sync — page_version baseline dla patchy.
710
- try {
711
- const files = await patchesPages.listMarkdownFiles();
712
- for (const relPath of files) {
713
- if (pageVersions.hasAny(relPath))
714
- continue;
715
- await pageVersions.recordVersion(relPath, 'create', 'filesystem', undefined, patchesSerializer, 'patch');
716
- }
717
- }
718
- catch (err) {
719
- console.warn('[page-version] patches initial sync failed:', err.message);
720
- }
721
- try {
722
- await pagesFrontmatterIndexer.indexAll();
723
- }
724
- catch (err) {
725
- console.warn('[pages-frontmatter-indexer] initial sync failed:', err.message);
726
- }
727
- })();
728
- const closeAssets = mode === 'dev' ? await mountDevVite(app, cwd) : mountProd(app, cwd);
220
+ app.use('/api', workspaceRouter({ registry, workspace, cache, mode, activateProject }));
221
+ app.use('/api/projects/:id', projectDispatchMiddleware(registry, workspace, cache));
222
+ // Eager warm of the CLI-started project: clone executes before listen and a
223
+ // broken initial config still fails the boot fast (parity with pre-M31). A
224
+ // workspace-only start has no initial project to warm.
225
+ const initialCtx = initialProject ? await cache.get(initialProject) : null;
226
+ const spaDeps = { registry, workspace, startCwd: cwd };
227
+ const closeAssets = mode === 'dev' ? await mountDevVite(app, spaDeps) : mountProd(app, spaDeps);
729
228
  const port = await listenOrExit(httpServer, portRef.current);
730
229
  portRef.current = port;
230
+ if (initialProject)
231
+ registry.touchLastOpened(workspace.name, initialProject.id);
731
232
  const url = `http://localhost:${port}`;
732
- const writingStyle = initialWritingStyle
733
- ? { slug: initialWritingStyle, title: skillRegistry.resolve(initialWritingStyle).metadata.title }
734
- : null;
735
233
  return {
736
234
  url,
737
235
  port,
738
- writingStyle,
236
+ writingStyle: initialCtx?.writingStyle ?? null,
739
237
  shutdown: async () => {
740
- await watcher.close();
741
- await briefsWatcher.close();
742
- await patchesWatcher.close();
238
+ await cache.disposeAll();
743
239
  await gateway.close();
744
240
  await closeAssets();
745
- db.close();
746
241
  await new Promise((resolve) => httpServer.close(() => resolve()));
747
242
  },
748
243
  };