specrails-desktop 2.8.0 → 2.9.1

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 (334) hide show
  1. package/README.md +23 -19
  2. package/client/dist/assets/{ActivityFeedPage-LKqd18-G.js → ActivityFeedPage-DpQzYMBz.js} +1 -1
  3. package/client/dist/assets/{AgentsPage-Cb-b-6Ot.js → AgentsPage-29fCY8qV.js} +1 -1
  4. package/client/dist/assets/{AnalyticsPage-HVxQQ1wy.js → AnalyticsPage-BwGtS6Hf.js} +1 -1
  5. package/client/dist/assets/{BarChart-BOyHB0dw.js → BarChart-CTR97DVC.js} +1 -1
  6. package/client/dist/assets/{CodePage-DnOnwKGB.js → CodePage-yAAxKasA.js} +1 -1
  7. package/client/dist/assets/{DesktopAnalyticsPage-D2auU39x.js → DesktopAnalyticsPage-BdK_XpsD.js} +1 -1
  8. package/client/dist/assets/{DocsDialog-CTuDX3GK.js → DocsDialog-BaE0cLlL.js} +2 -2
  9. package/client/dist/assets/{DocsPage-DRyMmu0Z.js → DocsPage-c1FgZX8_.js} +2 -2
  10. package/client/dist/assets/{ExportDropdown-DO-GGiMh.js → ExportDropdown-lPv_yDen.js} +1 -1
  11. package/client/dist/assets/{IntegrationsPage-BhbO4jFT.js → IntegrationsPage-DOpxRe7G.js} +1 -1
  12. package/client/dist/assets/{JobDetailPage-DJooEg1s.js → JobDetailPage-5ExzXY-F.js} +1 -1
  13. package/client/dist/assets/{JobsPage-BbaC-YOg.js → JobsPage-iW7WuPAc.js} +1 -1
  14. package/client/dist/assets/{dist-js-Xc2lRKp2.js → dist-js-A8aSaLng.js} +1 -1
  15. package/client/dist/assets/{dist-js-CiIVMsx3.js → dist-js-CD_m3Xj5.js} +1 -1
  16. package/client/dist/assets/index-D6BaYRRU.css +2 -0
  17. package/client/dist/assets/{index-DK214dak.js → index-DRhFPNAv.js} +44 -44
  18. package/client/dist/assets/{integrations-2C7MkGT0.js → integrations-7YyTBuU9.js} +1 -1
  19. package/client/dist/assets/{integrations-CX4p_bij.js → integrations-B9CEpNF0.js} +1 -1
  20. package/client/dist/assets/{integrations-C2jQtv-s.js → integrations-BlvAdewo.js} +1 -1
  21. package/client/dist/assets/{integrations-eQPHAYsE.js → integrations-Bw8UM9Xd.js} +1 -1
  22. package/client/dist/assets/{integrations-BDC670cg.js → integrations-C5SxNKnG.js} +1 -1
  23. package/client/dist/assets/{integrations-BqUmRUef.js → integrations-CJQKMmdW.js} +1 -1
  24. package/client/dist/assets/{integrations-CB98NeH5.js → integrations-DWz1eU_K.js} +1 -1
  25. package/client/dist/assets/{integrations-_SuVeQIG.js → integrations-DiPR8Fzp.js} +1 -1
  26. package/client/dist/assets/{lib-Bo5s6xpe.js → lib-1vkTuLY7.js} +1 -1
  27. package/client/dist/assets/setup-B6egeeTM.js +1 -0
  28. package/client/dist/assets/setup-BHroXlke.js +1 -0
  29. package/client/dist/assets/setup-BIXsWUp1.js +1 -0
  30. package/client/dist/assets/setup-BJRdg1iE.js +1 -0
  31. package/client/dist/assets/setup-C0rVGnCy.js +1 -0
  32. package/client/dist/assets/setup-Cpu17hJv.js +1 -0
  33. package/client/dist/assets/setup-D-1r0uSx.js +1 -0
  34. package/client/dist/assets/setup-Dn2-veYO.js +1 -0
  35. package/client/dist/assets/{tickets-9kdPXInd.js → tickets-CG_mo-Bg.js} +1 -1
  36. package/client/dist/assets/{tickets-n23kDqJT.js → tickets-CVJQ-vRm.js} +1 -1
  37. package/client/dist/assets/{tickets-tGx5AR5b.js → tickets-D5MSAPe_.js} +1 -1
  38. package/client/dist/assets/{tickets-1UIGf_oA.js → tickets-DBV3wgQZ.js} +1 -1
  39. package/client/dist/assets/{tickets-DNmXcAwu.js → tickets-Q0_pONEh.js} +1 -1
  40. package/client/dist/assets/{tickets-C6pwZwt4.js → tickets-RZ0LyeQe.js} +1 -1
  41. package/client/dist/assets/{tickets-DAjtxAVb.js → tickets-d1A6EOHa.js} +1 -1
  42. package/client/dist/assets/{tickets-0rM0lIXd.js → tickets-r4-oNV0R.js} +1 -1
  43. package/client/dist/assets/{useProjectCache-DVNypkmR.js → useProjectCache-CSi2xHri.js} +1 -1
  44. package/client/dist/index.html +5 -5
  45. package/docs/README.md +5 -2
  46. package/docs/agy-cli-provider-study.md +78 -0
  47. package/docs/cli.md +23 -4
  48. package/docs/codex.md +116 -58
  49. package/docs/creating-specs.md +19 -5
  50. package/docs/customizing.md +27 -6
  51. package/docs/gemini.md +225 -73
  52. package/docs/getting-started.md +18 -9
  53. package/docs/guide/de/agents/1-meet-the-agents.md +38 -0
  54. package/docs/guide/de/agents/2-profiles-and-the-balanced-default.md +45 -0
  55. package/docs/guide/de/agents/3-customizing-models-per-agent.md +60 -0
  56. package/docs/guide/de/agents/4-custom-agents-catalog.md +43 -0
  57. package/docs/guide/de/getting-started/1-what-is-specrails.md +49 -0
  58. package/docs/guide/de/getting-started/2-installing-and-first-run.md +42 -0
  59. package/docs/guide/de/getting-started/3-adding-your-first-project.md +58 -0
  60. package/docs/guide/de/getting-started/4-the-dashboard-tour.md +53 -0
  61. package/docs/guide/de/insights/1-analytics-and-cost-tracking.md +78 -0
  62. package/docs/guide/de/insights/2-the-integrated-terminal.md +46 -0
  63. package/docs/guide/de/insights/3-code-explorer.md +50 -0
  64. package/docs/guide/de/integrations/1-ai-providers.md +64 -0
  65. package/docs/guide/de/integrations/2-plugins.md +44 -0
  66. package/docs/guide/de/integrations/3-jira-integration.md +71 -0
  67. package/docs/guide/de/integrations/4-mobile-companion.md +38 -0
  68. package/docs/guide/de/pipeline/1-rails-and-jobs.md +94 -0
  69. package/docs/guide/de/pipeline/2-the-job-detail-view.md +90 -0
  70. package/docs/guide/de/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  71. package/docs/guide/de/pipeline/4-picking-an-engine-per-rail.md +60 -0
  72. package/docs/guide/de/settings/1-themes.md +37 -0
  73. package/docs/guide/de/settings/2-language.md +39 -0
  74. package/docs/guide/de/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  75. package/docs/guide/de/settings/4-where-your-data-lives.md +48 -0
  76. package/docs/guide/de/specs/1-specs-and-the-backlog.md +52 -0
  77. package/docs/guide/de/specs/2-add-spec-quick-mode.md +45 -0
  78. package/docs/guide/de/specs/3-add-spec-explore-mode.md +68 -0
  79. package/docs/guide/de/specs/4-drafts-and-contract-layer.md +81 -0
  80. package/docs/guide/en/agents/1-meet-the-agents.md +38 -0
  81. package/docs/guide/en/agents/2-profiles-and-the-balanced-default.md +45 -0
  82. package/docs/guide/en/agents/3-customizing-models-per-agent.md +60 -0
  83. package/docs/guide/en/agents/4-custom-agents-catalog.md +43 -0
  84. package/docs/guide/en/getting-started/1-what-is-specrails.md +49 -0
  85. package/docs/guide/en/getting-started/2-installing-and-first-run.md +42 -0
  86. package/docs/guide/en/getting-started/3-adding-your-first-project.md +58 -0
  87. package/docs/guide/en/getting-started/4-the-dashboard-tour.md +53 -0
  88. package/docs/guide/en/insights/1-analytics-and-cost-tracking.md +78 -0
  89. package/docs/guide/en/insights/2-the-integrated-terminal.md +46 -0
  90. package/docs/guide/en/insights/3-code-explorer.md +50 -0
  91. package/docs/guide/en/integrations/1-ai-providers.md +64 -0
  92. package/docs/guide/en/integrations/2-plugins.md +44 -0
  93. package/docs/guide/en/integrations/3-jira-integration.md +71 -0
  94. package/docs/guide/en/integrations/4-mobile-companion.md +38 -0
  95. package/docs/guide/en/pipeline/1-rails-and-jobs.md +94 -0
  96. package/docs/guide/en/pipeline/2-the-job-detail-view.md +90 -0
  97. package/docs/guide/en/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  98. package/docs/guide/en/pipeline/4-picking-an-engine-per-rail.md +60 -0
  99. package/docs/guide/en/settings/1-themes.md +37 -0
  100. package/docs/guide/en/settings/2-language.md +39 -0
  101. package/docs/guide/en/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  102. package/docs/guide/en/settings/4-where-your-data-lives.md +48 -0
  103. package/docs/guide/en/specs/1-specs-and-the-backlog.md +52 -0
  104. package/docs/guide/en/specs/2-add-spec-quick-mode.md +45 -0
  105. package/docs/guide/en/specs/3-add-spec-explore-mode.md +68 -0
  106. package/docs/guide/en/specs/4-drafts-and-contract-layer.md +81 -0
  107. package/docs/guide/es/agents/1-meet-the-agents.md +38 -0
  108. package/docs/guide/es/agents/2-profiles-and-the-balanced-default.md +45 -0
  109. package/docs/guide/es/agents/3-customizing-models-per-agent.md +60 -0
  110. package/docs/guide/es/agents/4-custom-agents-catalog.md +43 -0
  111. package/docs/guide/es/getting-started/1-what-is-specrails.md +49 -0
  112. package/docs/guide/es/getting-started/2-installing-and-first-run.md +42 -0
  113. package/docs/guide/es/getting-started/3-adding-your-first-project.md +58 -0
  114. package/docs/guide/es/getting-started/4-the-dashboard-tour.md +53 -0
  115. package/docs/guide/es/insights/1-analytics-and-cost-tracking.md +78 -0
  116. package/docs/guide/es/insights/2-the-integrated-terminal.md +46 -0
  117. package/docs/guide/es/insights/3-code-explorer.md +50 -0
  118. package/docs/guide/es/integrations/1-ai-providers.md +64 -0
  119. package/docs/guide/es/integrations/2-plugins.md +44 -0
  120. package/docs/guide/es/integrations/3-jira-integration.md +71 -0
  121. package/docs/guide/es/integrations/4-mobile-companion.md +38 -0
  122. package/docs/guide/es/pipeline/1-rails-and-jobs.md +94 -0
  123. package/docs/guide/es/pipeline/2-the-job-detail-view.md +90 -0
  124. package/docs/guide/es/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  125. package/docs/guide/es/pipeline/4-picking-an-engine-per-rail.md +60 -0
  126. package/docs/guide/es/settings/1-themes.md +37 -0
  127. package/docs/guide/es/settings/2-language.md +39 -0
  128. package/docs/guide/es/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  129. package/docs/guide/es/settings/4-where-your-data-lives.md +48 -0
  130. package/docs/guide/es/specs/1-specs-and-the-backlog.md +52 -0
  131. package/docs/guide/es/specs/2-add-spec-quick-mode.md +45 -0
  132. package/docs/guide/es/specs/3-add-spec-explore-mode.md +68 -0
  133. package/docs/guide/es/specs/4-drafts-and-contract-layer.md +81 -0
  134. package/docs/guide/fr/agents/1-meet-the-agents.md +38 -0
  135. package/docs/guide/fr/agents/2-profiles-and-the-balanced-default.md +45 -0
  136. package/docs/guide/fr/agents/3-customizing-models-per-agent.md +60 -0
  137. package/docs/guide/fr/agents/4-custom-agents-catalog.md +43 -0
  138. package/docs/guide/fr/getting-started/1-what-is-specrails.md +49 -0
  139. package/docs/guide/fr/getting-started/2-installing-and-first-run.md +42 -0
  140. package/docs/guide/fr/getting-started/3-adding-your-first-project.md +58 -0
  141. package/docs/guide/fr/getting-started/4-the-dashboard-tour.md +53 -0
  142. package/docs/guide/fr/insights/1-analytics-and-cost-tracking.md +78 -0
  143. package/docs/guide/fr/insights/2-the-integrated-terminal.md +46 -0
  144. package/docs/guide/fr/insights/3-code-explorer.md +50 -0
  145. package/docs/guide/fr/integrations/1-ai-providers.md +64 -0
  146. package/docs/guide/fr/integrations/2-plugins.md +44 -0
  147. package/docs/guide/fr/integrations/3-jira-integration.md +71 -0
  148. package/docs/guide/fr/integrations/4-mobile-companion.md +38 -0
  149. package/docs/guide/fr/pipeline/1-rails-and-jobs.md +94 -0
  150. package/docs/guide/fr/pipeline/2-the-job-detail-view.md +90 -0
  151. package/docs/guide/fr/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  152. package/docs/guide/fr/pipeline/4-picking-an-engine-per-rail.md +60 -0
  153. package/docs/guide/fr/settings/1-themes.md +37 -0
  154. package/docs/guide/fr/settings/2-language.md +39 -0
  155. package/docs/guide/fr/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  156. package/docs/guide/fr/settings/4-where-your-data-lives.md +48 -0
  157. package/docs/guide/fr/specs/1-specs-and-the-backlog.md +52 -0
  158. package/docs/guide/fr/specs/2-add-spec-quick-mode.md +45 -0
  159. package/docs/guide/fr/specs/3-add-spec-explore-mode.md +68 -0
  160. package/docs/guide/fr/specs/4-drafts-and-contract-layer.md +81 -0
  161. package/docs/guide/it/agents/1-meet-the-agents.md +38 -0
  162. package/docs/guide/it/agents/2-profiles-and-the-balanced-default.md +45 -0
  163. package/docs/guide/it/agents/3-customizing-models-per-agent.md +60 -0
  164. package/docs/guide/it/agents/4-custom-agents-catalog.md +43 -0
  165. package/docs/guide/it/getting-started/1-what-is-specrails.md +49 -0
  166. package/docs/guide/it/getting-started/2-installing-and-first-run.md +42 -0
  167. package/docs/guide/it/getting-started/3-adding-your-first-project.md +58 -0
  168. package/docs/guide/it/getting-started/4-the-dashboard-tour.md +53 -0
  169. package/docs/guide/it/insights/1-analytics-and-cost-tracking.md +78 -0
  170. package/docs/guide/it/insights/2-the-integrated-terminal.md +46 -0
  171. package/docs/guide/it/insights/3-code-explorer.md +50 -0
  172. package/docs/guide/it/integrations/1-ai-providers.md +64 -0
  173. package/docs/guide/it/integrations/2-plugins.md +44 -0
  174. package/docs/guide/it/integrations/3-jira-integration.md +71 -0
  175. package/docs/guide/it/integrations/4-mobile-companion.md +38 -0
  176. package/docs/guide/it/pipeline/1-rails-and-jobs.md +94 -0
  177. package/docs/guide/it/pipeline/2-the-job-detail-view.md +90 -0
  178. package/docs/guide/it/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  179. package/docs/guide/it/pipeline/4-picking-an-engine-per-rail.md +60 -0
  180. package/docs/guide/it/settings/1-themes.md +37 -0
  181. package/docs/guide/it/settings/2-language.md +39 -0
  182. package/docs/guide/it/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  183. package/docs/guide/it/settings/4-where-your-data-lives.md +48 -0
  184. package/docs/guide/it/specs/1-specs-and-the-backlog.md +52 -0
  185. package/docs/guide/it/specs/2-add-spec-quick-mode.md +45 -0
  186. package/docs/guide/it/specs/3-add-spec-explore-mode.md +68 -0
  187. package/docs/guide/it/specs/4-drafts-and-contract-layer.md +81 -0
  188. package/docs/guide/ja/agents/1-meet-the-agents.md +38 -0
  189. package/docs/guide/ja/agents/2-profiles-and-the-balanced-default.md +45 -0
  190. package/docs/guide/ja/agents/3-customizing-models-per-agent.md +60 -0
  191. package/docs/guide/ja/agents/4-custom-agents-catalog.md +43 -0
  192. package/docs/guide/ja/getting-started/1-what-is-specrails.md +49 -0
  193. package/docs/guide/ja/getting-started/2-installing-and-first-run.md +42 -0
  194. package/docs/guide/ja/getting-started/3-adding-your-first-project.md +58 -0
  195. package/docs/guide/ja/getting-started/4-the-dashboard-tour.md +53 -0
  196. package/docs/guide/ja/insights/1-analytics-and-cost-tracking.md +78 -0
  197. package/docs/guide/ja/insights/2-the-integrated-terminal.md +46 -0
  198. package/docs/guide/ja/insights/3-code-explorer.md +50 -0
  199. package/docs/guide/ja/integrations/1-ai-providers.md +64 -0
  200. package/docs/guide/ja/integrations/2-plugins.md +44 -0
  201. package/docs/guide/ja/integrations/3-jira-integration.md +71 -0
  202. package/docs/guide/ja/integrations/4-mobile-companion.md +38 -0
  203. package/docs/guide/ja/pipeline/1-rails-and-jobs.md +94 -0
  204. package/docs/guide/ja/pipeline/2-the-job-detail-view.md +90 -0
  205. package/docs/guide/ja/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  206. package/docs/guide/ja/pipeline/4-picking-an-engine-per-rail.md +60 -0
  207. package/docs/guide/ja/settings/1-themes.md +37 -0
  208. package/docs/guide/ja/settings/2-language.md +39 -0
  209. package/docs/guide/ja/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  210. package/docs/guide/ja/settings/4-where-your-data-lives.md +48 -0
  211. package/docs/guide/ja/specs/1-specs-and-the-backlog.md +52 -0
  212. package/docs/guide/ja/specs/2-add-spec-quick-mode.md +45 -0
  213. package/docs/guide/ja/specs/3-add-spec-explore-mode.md +68 -0
  214. package/docs/guide/ja/specs/4-drafts-and-contract-layer.md +81 -0
  215. package/docs/guide/pt/agents/1-meet-the-agents.md +38 -0
  216. package/docs/guide/pt/agents/2-profiles-and-the-balanced-default.md +45 -0
  217. package/docs/guide/pt/agents/3-customizing-models-per-agent.md +60 -0
  218. package/docs/guide/pt/agents/4-custom-agents-catalog.md +43 -0
  219. package/docs/guide/pt/getting-started/1-what-is-specrails.md +49 -0
  220. package/docs/guide/pt/getting-started/2-installing-and-first-run.md +42 -0
  221. package/docs/guide/pt/getting-started/3-adding-your-first-project.md +58 -0
  222. package/docs/guide/pt/getting-started/4-the-dashboard-tour.md +53 -0
  223. package/docs/guide/pt/insights/1-analytics-and-cost-tracking.md +78 -0
  224. package/docs/guide/pt/insights/2-the-integrated-terminal.md +46 -0
  225. package/docs/guide/pt/insights/3-code-explorer.md +50 -0
  226. package/docs/guide/pt/integrations/1-ai-providers.md +64 -0
  227. package/docs/guide/pt/integrations/2-plugins.md +44 -0
  228. package/docs/guide/pt/integrations/3-jira-integration.md +71 -0
  229. package/docs/guide/pt/integrations/4-mobile-companion.md +38 -0
  230. package/docs/guide/pt/pipeline/1-rails-and-jobs.md +94 -0
  231. package/docs/guide/pt/pipeline/2-the-job-detail-view.md +90 -0
  232. package/docs/guide/pt/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  233. package/docs/guide/pt/pipeline/4-picking-an-engine-per-rail.md +60 -0
  234. package/docs/guide/pt/settings/1-themes.md +37 -0
  235. package/docs/guide/pt/settings/2-language.md +39 -0
  236. package/docs/guide/pt/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  237. package/docs/guide/pt/settings/4-where-your-data-lives.md +48 -0
  238. package/docs/guide/pt/specs/1-specs-and-the-backlog.md +52 -0
  239. package/docs/guide/pt/specs/2-add-spec-quick-mode.md +45 -0
  240. package/docs/guide/pt/specs/3-add-spec-explore-mode.md +68 -0
  241. package/docs/guide/pt/specs/4-drafts-and-contract-layer.md +81 -0
  242. package/docs/guide/zh/agents/1-meet-the-agents.md +38 -0
  243. package/docs/guide/zh/agents/2-profiles-and-the-balanced-default.md +45 -0
  244. package/docs/guide/zh/agents/3-customizing-models-per-agent.md +60 -0
  245. package/docs/guide/zh/agents/4-custom-agents-catalog.md +43 -0
  246. package/docs/guide/zh/getting-started/1-what-is-specrails.md +49 -0
  247. package/docs/guide/zh/getting-started/2-installing-and-first-run.md +42 -0
  248. package/docs/guide/zh/getting-started/3-adding-your-first-project.md +58 -0
  249. package/docs/guide/zh/getting-started/4-the-dashboard-tour.md +53 -0
  250. package/docs/guide/zh/insights/1-analytics-and-cost-tracking.md +78 -0
  251. package/docs/guide/zh/insights/2-the-integrated-terminal.md +46 -0
  252. package/docs/guide/zh/insights/3-code-explorer.md +50 -0
  253. package/docs/guide/zh/integrations/1-ai-providers.md +64 -0
  254. package/docs/guide/zh/integrations/2-plugins.md +44 -0
  255. package/docs/guide/zh/integrations/3-jira-integration.md +71 -0
  256. package/docs/guide/zh/integrations/4-mobile-companion.md +38 -0
  257. package/docs/guide/zh/pipeline/1-rails-and-jobs.md +94 -0
  258. package/docs/guide/zh/pipeline/2-the-job-detail-view.md +90 -0
  259. package/docs/guide/zh/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  260. package/docs/guide/zh/pipeline/4-picking-an-engine-per-rail.md +60 -0
  261. package/docs/guide/zh/settings/1-themes.md +37 -0
  262. package/docs/guide/zh/settings/2-language.md +39 -0
  263. package/docs/guide/zh/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  264. package/docs/guide/zh/settings/4-where-your-data-lives.md +48 -0
  265. package/docs/guide/zh/specs/1-specs-and-the-backlog.md +52 -0
  266. package/docs/guide/zh/specs/2-add-spec-quick-mode.md +45 -0
  267. package/docs/guide/zh/specs/3-add-spec-explore-mode.md +68 -0
  268. package/docs/guide/zh/specs/4-drafts-and-contract-layer.md +81 -0
  269. package/docs/internals/README.md +1 -1
  270. package/docs/internals/adding-a-provider.md +192 -59
  271. package/docs/internals/api-reference.md +130 -21
  272. package/docs/internals/architecture.md +22 -9
  273. package/docs/internals/bundled-framework-build-plan.md +264 -0
  274. package/docs/internals/configuration.md +33 -8
  275. package/docs/internals/global-artifacts-alignment-contract.md +486 -0
  276. package/docs/internals/global-artifacts-relocation-evaluation.md +294 -0
  277. package/docs/internals/operations-runbook.md +16 -5
  278. package/docs/internals/profiles.md +42 -14
  279. package/docs/platforms/macos.md +27 -8
  280. package/docs/platforms/windows.md +20 -6
  281. package/docs/running-pipelines.md +17 -9
  282. package/docs/terminal.md +9 -3
  283. package/docs/tracking-cost.md +17 -11
  284. package/package.json +1 -1
  285. package/server/dist/agent-refine-manager.js +20 -5
  286. package/server/dist/artifact-registry.js +468 -0
  287. package/server/dist/attachment-manager.js +5 -8
  288. package/server/dist/browser-capture-manager.js +4 -4
  289. package/server/dist/bundled-core.js +72 -0
  290. package/server/dist/bundled-openspec.js +58 -0
  291. package/server/dist/chat-manager.js +42 -5
  292. package/server/dist/code-explorer-router.js +10 -7
  293. package/server/dist/config.js +7 -2
  294. package/server/dist/context-budget.js +17 -6
  295. package/server/dist/context-scope.js +6 -2
  296. package/server/dist/contract-refine-runner.js +31 -9
  297. package/server/dist/desktop-router.js +24 -1
  298. package/server/dist/docs-router.js +210 -132
  299. package/server/dist/file-summary-manager.js +41 -16
  300. package/server/dist/framework-manager.js +248 -0
  301. package/server/dist/framework-migration.js +308 -0
  302. package/server/dist/index.js +30 -0
  303. package/server/dist/install-config-path.js +73 -0
  304. package/server/dist/jira/jira-sync-manager.js +23 -11
  305. package/server/dist/openspec-shim.js +153 -0
  306. package/server/dist/plugins-router.js +19 -8
  307. package/server/dist/profiles-router.js +38 -16
  308. package/server/dist/project-registry.js +101 -3
  309. package/server/dist/project-router-chat.js +1 -1
  310. package/server/dist/project-router-helpers.js +25 -12
  311. package/server/dist/project-router-jobs.js +3 -3
  312. package/server/dist/project-router-settings.js +8 -6
  313. package/server/dist/project-router-setup.js +27 -10
  314. package/server/dist/project-router-spending.js +6 -1
  315. package/server/dist/project-router-tickets.js +30 -10
  316. package/server/dist/project-router.js +16 -1
  317. package/server/dist/queue-manager.js +149 -18
  318. package/server/dist/setup-manager.js +131 -29
  319. package/server/dist/smash-runner.js +21 -6
  320. package/server/dist/ticket-store.js +6 -2
  321. package/server/dist/ticket-watcher.js +5 -1
  322. package/server/dist/vitest-setup.js +25 -0
  323. package/server/dist/workspace-manager.js +199 -0
  324. package/server/dist/workspace-resolution.js +147 -0
  325. package/client/dist/assets/index-DgKfQFcf.css +0 -2
  326. package/client/dist/assets/setup-BIIkb-_K.js +0 -1
  327. package/client/dist/assets/setup-BeQxu9kD.js +0 -1
  328. package/client/dist/assets/setup-CPa6GnlI.js +0 -1
  329. package/client/dist/assets/setup-CZl4OEJx.js +0 -1
  330. package/client/dist/assets/setup-ChpodNfn.js +0 -1
  331. package/client/dist/assets/setup-D_fjJH6u.js +0 -1
  332. package/client/dist/assets/setup-YzD8DX4O.js +0 -1
  333. package/client/dist/assets/setup-fRpDozmq.js +0 -1
  334. package/docs/adding-a-provider.md +0 -107
@@ -0,0 +1,468 @@
1
+ "use strict";
2
+ /**
3
+ * Shared artifact registry — the cross-tool contract that lets specrails-core
4
+ * place, and specrails-desktop write/read, a repo's relocated artifacts OUTSIDE
5
+ * the repo, under `$HOME/.specrails/projects/<slug>/workspace`.
6
+ *
7
+ * Single source of truth: `$HOME/.specrails/registry.json`, an inspectable,
8
+ * schema-versioned JSON file keyed by the **canonical realpath of the repo**.
9
+ * specrails-desktop is the PRIMARY writer (a projection of its `desktop.sqlite`);
10
+ * specrails-core reads it and, when run standalone, allocates its own entry.
11
+ *
12
+ * This module is a port of specrails-core's `src/installer/util/registry.ts`.
13
+ * The slug algorithm, canonical-key rule, atomic-write rule, lock protocol and
14
+ * workspace layout here MUST stay byte-identical to core — the correctness of
15
+ * the cross-tool contract depends on both tools resolving the same repo to the
16
+ * same paths. Everything is synchronous to match the installer's sync flow and
17
+ * to keep desktop's startup reconcile cheap.
18
+ *
19
+ * Beyond the ported core surface, this module adds the desktop-only writers that
20
+ * project `desktop.sqlite` into the registry: `mirrorProjectEntry`,
21
+ * `removeRegistryEntry`, and `reconcileFromProjects`.
22
+ */
23
+ var __importDefault = (this && this.__importDefault) || function (mod) {
24
+ return (mod && mod.__esModule) ? mod : { "default": mod };
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.REGISTRY_SCHEMA_VERSION = void 0;
28
+ exports.slugify = slugify;
29
+ exports.resolveHome = resolveHome;
30
+ exports.registryPath = registryPath;
31
+ exports.lockPath = lockPath;
32
+ exports.realpathSafe = realpathSafe;
33
+ exports.normalizeKey = normalizeKey;
34
+ exports.canonicalizeRepoPath = canonicalizeRepoPath;
35
+ exports.workspaceLayout = workspaceLayout;
36
+ exports.isCompleteEntry = isCompleteEntry;
37
+ exports.readRegistryOrEmpty = readRegistryOrEmpty;
38
+ exports.atomicWrite = atomicWrite;
39
+ exports.withFileLock = withFileLock;
40
+ exports.mirrorProjectEntry = mirrorProjectEntry;
41
+ exports.resolveArtifacts = resolveArtifacts;
42
+ exports.removeRegistryEntry = removeRegistryEntry;
43
+ exports.reconcileFromProjects = reconcileFromProjects;
44
+ const fs_1 = require("fs");
45
+ const os_1 = __importDefault(require("os"));
46
+ const path_1 = __importDefault(require("path"));
47
+ /** Registry schema version. A reader that sees a higher value MUST treat all
48
+ * entries as absent (legacy fallback), never mis-parse. */
49
+ exports.REGISTRY_SCHEMA_VERSION = 1;
50
+ /**
51
+ * Slug derivation — byte-identical to `slugify` in `server/desktop-router.ts`
52
+ * (and to specrails-core's port). Do NOT "improve" it; parity is the contract.
53
+ */
54
+ function slugify(name) {
55
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
56
+ }
57
+ /** `$HOME` for the registry, overridable for tests. Honors the
58
+ * `SPECRAILS_REGISTRY_HOME` env var (mirrors core) when no explicit override
59
+ * is passed, so the desktop-side wiring can be redirected in tests without
60
+ * threading a `home` argument through every call site. */
61
+ function resolveHome(home) {
62
+ return home ?? process.env.SPECRAILS_REGISTRY_HOME ?? os_1.default.homedir();
63
+ }
64
+ /** Absolute path to `registry.json`. */
65
+ function registryPath(home) {
66
+ return path_1.default.join(resolveHome(home), '.specrails', 'registry.json');
67
+ }
68
+ /** Absolute path to the advisory lock file. */
69
+ function lockPath(home) {
70
+ return registryPath(home) + '.lock';
71
+ }
72
+ /** `fs.realpathSync` that falls back to the resolved-but-unreal path on error
73
+ * (the path may not exist yet, or be on a volume that rejects realpath). */
74
+ function realpathSafe(abs) {
75
+ try {
76
+ return (0, fs_1.realpathSync)(abs);
77
+ }
78
+ catch {
79
+ return abs;
80
+ }
81
+ }
82
+ /** Case-fold the key on case-insensitive platforms (macOS, Windows) so two
83
+ * spellings of the same path map to one entry. The stored `repoPath` keeps
84
+ * its canonical case; only the index key is folded. */
85
+ function normalizeKey(canon) {
86
+ if (process.platform === 'darwin' || process.platform === 'win32') {
87
+ return canon.toLowerCase();
88
+ }
89
+ return canon;
90
+ }
91
+ /** Canonical repo path: resolve to absolute, then realpath (collapses symlinks). */
92
+ function canonicalizeRepoPath(repoPathInput) {
93
+ return realpathSafe(path_1.default.resolve(repoPathInput));
94
+ }
95
+ /** The per-project sub-path layout under a workspace dir. Single source of the
96
+ * layout so writer and (allocation) reader never disagree. */
97
+ function workspaceLayout(home, slug, canon) {
98
+ const workspaceDir = path_1.default.join(resolveHome(home), '.specrails', 'projects', slug, 'workspace');
99
+ const specrailsDir = path_1.default.join(workspaceDir, '.specrails');
100
+ return {
101
+ repoPath: canon,
102
+ slug,
103
+ workspaceDir,
104
+ artifactRoot: workspaceDir,
105
+ codeRoot: canon,
106
+ stateDir: path_1.default.join(workspaceDir, '.claude'),
107
+ ticketsPath: path_1.default.join(specrailsDir, 'local-tickets.json'),
108
+ backlogConfigPath: path_1.default.join(specrailsDir, 'backlog-config.json'),
109
+ profilesDir: path_1.default.join(specrailsDir, 'profiles'),
110
+ pluginsStateDir: path_1.default.join(specrailsDir, 'plugins'),
111
+ fileSummariesDir: path_1.default.join(specrailsDir, 'file-summaries'),
112
+ };
113
+ }
114
+ /** The path/identity fields a usable entry MUST carry. A hand-edited or
115
+ * partially-written entry missing ANY of these is treated as ABSENT (legacy
116
+ * fallback) rather than returned with `undefined` path fields that would crash
117
+ * a downstream `fs` call or silently point at `undefined/...`. */
118
+ const REQUIRED_ENTRY_FIELDS = [
119
+ 'repoPath',
120
+ 'slug',
121
+ 'workspaceDir',
122
+ 'artifactRoot',
123
+ 'codeRoot',
124
+ 'stateDir',
125
+ 'ticketsPath',
126
+ 'backlogConfigPath',
127
+ 'profilesDir',
128
+ 'pluginsStateDir',
129
+ 'fileSummariesDir',
130
+ ];
131
+ /**
132
+ * True only when `entry` carries every required path/identity field as a
133
+ * non-empty string. Guards against a hand-edited registry.json with a missing
134
+ * key (which would otherwise surface as `ticketsPath: undefined`).
135
+ */
136
+ function isCompleteEntry(entry) {
137
+ if (!entry || typeof entry !== 'object')
138
+ return false;
139
+ const e = entry;
140
+ for (const f of REQUIRED_ENTRY_FIELDS) {
141
+ const v = e[f];
142
+ if (typeof v !== 'string' || v.length === 0)
143
+ return false;
144
+ }
145
+ return true;
146
+ }
147
+ /**
148
+ * Total, fail-open read. A missing file, a parse error, or a `schemaVersion`
149
+ * greater than we understand all yield an empty registry, so a caller treats
150
+ * it as "no entry" and (if writing) writes a fresh, understood entry rather
151
+ * than crashing. Biases toward availability over strict consistency — correct
152
+ * for a local, inspectable file.
153
+ */
154
+ function readRegistryOrEmpty(home) {
155
+ const empty = { schemaVersion: exports.REGISTRY_SCHEMA_VERSION, projects: {} };
156
+ const p = registryPath(home);
157
+ if (!(0, fs_1.existsSync)(p))
158
+ return empty;
159
+ try {
160
+ const parsed = JSON.parse((0, fs_1.readFileSync)(p, 'utf8'));
161
+ if (!parsed ||
162
+ typeof parsed !== 'object' ||
163
+ typeof parsed.schemaVersion !== 'number' ||
164
+ parsed.schemaVersion > exports.REGISTRY_SCHEMA_VERSION ||
165
+ typeof parsed.projects !== 'object' ||
166
+ parsed.projects === null) {
167
+ return empty;
168
+ }
169
+ return parsed;
170
+ }
171
+ catch {
172
+ return empty;
173
+ }
174
+ }
175
+ /** Write `data` to `filePath` atomically: temp file in the same dir, fsync,
176
+ * rename. A reader (even without the lock) only ever sees a complete old or
177
+ * new file — the lock serialises writers, the rename protects readers. */
178
+ function atomicWrite(filePath, data) {
179
+ const dir = path_1.default.dirname(filePath);
180
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
181
+ // Deterministic-but-unique temp name (no Math.random/Date dependency for the
182
+ // name itself); collisions are impossible because the lock serialises writers.
183
+ const tmp = path_1.default.join(dir, `.${path_1.default.basename(filePath)}.tmp-${process.pid}`);
184
+ const fd = (0, fs_1.openSync)(tmp, 'w');
185
+ try {
186
+ (0, fs_1.writeFileSync)(fd, data);
187
+ (0, fs_1.fsyncSync)(fd);
188
+ }
189
+ finally {
190
+ (0, fs_1.closeSync)(fd);
191
+ }
192
+ (0, fs_1.renameSync)(tmp, filePath);
193
+ }
194
+ /** Synchronous sleep without busy-spinning the CPU. */
195
+ function syncSleep(ms) {
196
+ const shared = new Int32Array(new SharedArrayBuffer(4));
197
+ Atomics.wait(shared, 0, 0, ms);
198
+ }
199
+ const LOCK_STALE_MS = 30_000;
200
+ const LOCK_SPIN_MS = 50;
201
+ const LOCK_MAX_WAIT_MS = 2_000;
202
+ /**
203
+ * Run `fn` while holding an advisory lock over the registry. Mutual exclusion
204
+ * is an exclusive-create lock file (`open(..., 'wx')`) with bounded spin-retry
205
+ * and stale-lock breaking (a lock whose mtime exceeds the TTL is reclaimed —
206
+ * covers a crashed writer). Read-only callers never take the lock.
207
+ */
208
+ function withFileLock(home, fn) {
209
+ const lp = lockPath(home);
210
+ (0, fs_1.mkdirSync)(path_1.default.dirname(lp), { recursive: true });
211
+ let fd;
212
+ // Unique per-acquisition token written INTO the lock file. On release we
213
+ // re-read the file and only unlink it when the token still matches OURS — so a
214
+ // writer whose lock was stale-broken (TTL-reclaimed) by another writer never
215
+ // deletes the NEW owner's lock out from under it (which would let a third
216
+ // writer acquire concurrently and race the read-modify-write).
217
+ const ourToken = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
218
+ const deadline = Date.now() + LOCK_MAX_WAIT_MS;
219
+ for (;;) {
220
+ try {
221
+ fd = (0, fs_1.openSync)(lp, 'wx');
222
+ // Write our token + fsync so a concurrent reader sees a complete token.
223
+ try {
224
+ (0, fs_1.writeFileSync)(fd, ourToken);
225
+ (0, fs_1.fsyncSync)(fd);
226
+ }
227
+ catch {
228
+ /* best-effort — the lock still serialises via exclusive-create */
229
+ }
230
+ break;
231
+ }
232
+ catch {
233
+ // Lock held — break it if stale, otherwise spin until the deadline.
234
+ try {
235
+ const age = Date.now() - (0, fs_1.statSync)(lp).mtimeMs;
236
+ if (age > LOCK_STALE_MS) {
237
+ (0, fs_1.unlinkSync)(lp);
238
+ continue;
239
+ }
240
+ }
241
+ catch {
242
+ // lock vanished between open and stat — retry immediately
243
+ continue;
244
+ }
245
+ if (Date.now() >= deadline) {
246
+ // FAIL-CLOSED (parity with specrails-core): rather than proceed WITHOUT
247
+ // the lock — which would let two writers race a read-modify-write and
248
+ // clobber each other's entries — throw. Every caller wraps this in a
249
+ // non-fatal try/catch (a missing/un-updated entry is recreated on the
250
+ // next reconcile), so failing closed loses nothing while preserving the
251
+ // single-writer invariant.
252
+ throw new Error(`artifact-registry: could not acquire lock ${lp} within ${LOCK_MAX_WAIT_MS}ms`);
253
+ }
254
+ syncSleep(LOCK_SPIN_MS);
255
+ }
256
+ }
257
+ try {
258
+ return fn();
259
+ }
260
+ finally {
261
+ if (fd !== undefined) {
262
+ try {
263
+ (0, fs_1.closeSync)(fd);
264
+ }
265
+ catch {
266
+ /* already closed */
267
+ }
268
+ // Only unlink the lock when it still carries OUR token. If we were
269
+ // stale-broken and another writer now owns the file, its token differs and
270
+ // we leave it alone (a missing/unreadable file ⇒ already gone ⇒ no-op).
271
+ try {
272
+ const onDisk = (0, fs_1.readFileSync)(lp, 'utf8');
273
+ if (onDisk === ourToken) {
274
+ (0, fs_1.unlinkSync)(lp);
275
+ }
276
+ }
277
+ catch {
278
+ /* already removed / unreadable — nothing to clean up */
279
+ }
280
+ }
281
+ }
282
+ }
283
+ function normalizeProviders(providers, primary) {
284
+ let list = providers && providers.length > 0 ? providers.slice() : undefined;
285
+ if (!list || list.length === 0) {
286
+ list = [primary && primary.length > 0 ? primary : 'claude'];
287
+ }
288
+ const primaryProvider = primary && primary.length > 0 ? primary : list[0];
289
+ return { providers: list, primaryProvider };
290
+ }
291
+ /**
292
+ * Build (or refresh) a desktop-owned entry for a repo, preserving artifact-
293
+ * location identity.
294
+ *
295
+ * INVARIANT: a `slug` + `workspaceDir` are **immutable once an entry exists**
296
+ * (the contract's "el slug queda congelado para siempre"). When ANY entry
297
+ * already exists for this repo — whether a prior desktop entry OR a
298
+ * `core-standalone` entry being adopted (possibly under a core-allocated slug
299
+ * that differs from desktop's) — we preserve EVERY path field verbatim and
300
+ * refresh only the mutable projection fields (providers, coreVersion, source,
301
+ * timestamps, desktopProjectId). Re-deriving the layout from `desiredSlug`
302
+ * would re-home an adopted/renamed entry and STRAND its on-disk artifacts.
303
+ *
304
+ * Only when no entry exists do we allocate fresh paths from `desiredSlug`.
305
+ *
306
+ * `touchInstall` = true for an install event (addProject) ⇒ bump
307
+ * `lastInstallAt`; false for a startup reconcile ⇒ leave it.
308
+ */
309
+ function buildMirroredEntry(existing, canon, desiredSlug, input, now, touchInstall, home) {
310
+ let entry;
311
+ if (existing) {
312
+ entry = {
313
+ ...existing,
314
+ providers: input.providers,
315
+ primaryProvider: input.primaryProvider,
316
+ coreVersion: input.coreVersion ?? existing.coreVersion,
317
+ source: 'desktop',
318
+ createdAt: existing.createdAt ?? now,
319
+ lastInstallAt: touchInstall ? now : (existing.lastInstallAt ?? now),
320
+ updatedAt: now,
321
+ };
322
+ }
323
+ else {
324
+ const layout = workspaceLayout(home ?? resolveHome(home), desiredSlug, canon);
325
+ entry = {
326
+ ...layout,
327
+ providers: input.providers,
328
+ primaryProvider: input.primaryProvider,
329
+ coreVersion: input.coreVersion,
330
+ source: 'desktop',
331
+ createdAt: now,
332
+ lastInstallAt: now,
333
+ updatedAt: now,
334
+ };
335
+ }
336
+ if (input.desktopProjectId)
337
+ entry.desktopProjectId = input.desktopProjectId;
338
+ return entry;
339
+ }
340
+ /**
341
+ * Upsert a desktop-owned entry for a repo, keyed by the canonical repo path.
342
+ *
343
+ * Behaviour:
344
+ * - new key ⇒ insert a fresh `source:'desktop'` entry, computing every sub-path
345
+ * from `workspaceLayout` with the desktop slug.
346
+ * - existing `source:'desktop'` entry ⇒ update providers/coreVersion/paths,
347
+ * preserve `createdAt`, refresh `lastInstallAt`/`updatedAt`.
348
+ * - existing `source:'core-standalone'` entry ⇒ ADOPT it: keep its slug +
349
+ * workspaceDir (core already materialised artifacts there), flip
350
+ * `source:'desktop'`, attach `desktopProjectId`. The workspace location is
351
+ * immutable post-adoption so we never strand on-disk artifacts.
352
+ *
353
+ * Runs under the file lock with a re-read so concurrent writers serialise.
354
+ */
355
+ function mirrorProjectEntry(input, home) {
356
+ const canon = canonicalizeRepoPath(input.repoPath);
357
+ const key = normalizeKey(canon);
358
+ const { providers, primaryProvider } = normalizeProviders(input.providers, input.primaryProvider);
359
+ return withFileLock(home, () => {
360
+ const reg = readRegistryOrEmpty(home);
361
+ const now = new Date().toISOString();
362
+ const existing = reg.projects[key];
363
+ const entry = buildMirroredEntry(existing, canon, input.slug, { providers, primaryProvider, coreVersion: input.coreVersion, desktopProjectId: input.desktopProjectId }, now, true, home);
364
+ reg.projects[key] = entry;
365
+ reg.schemaVersion = exports.REGISTRY_SCHEMA_VERSION;
366
+ reg.generator = 'specrails-desktop';
367
+ reg.updatedAt = now;
368
+ atomicWrite(registryPath(home), JSON.stringify(reg, null, 2) + '\n');
369
+ return entry;
370
+ });
371
+ }
372
+ /**
373
+ * Resolve a repo's artifact locations from the registry (read-only). Returns a
374
+ * legacy (repo-relative) resolution when no entry exists, so callers can treat
375
+ * the result uniformly. NEVER mutates the registry.
376
+ */
377
+ function resolveArtifacts(repoPath, _options, home) {
378
+ const canon = canonicalizeRepoPath(repoPath);
379
+ const key = normalizeKey(canon);
380
+ const reg = readRegistryOrEmpty(home);
381
+ const rawEntry = reg.projects[key];
382
+ // A hand-edited / partially-written entry missing a path field must fall back
383
+ // to legacy (repo-relative) — never return `ticketsPath: undefined`.
384
+ const entry = isCompleteEntry(rawEntry) ? rawEntry : undefined;
385
+ if (!entry) {
386
+ // Legacy: every artifact lives under the repo's own `.specrails`.
387
+ const specrailsDir = path_1.default.join(canon, '.specrails');
388
+ return {
389
+ isLegacy: true,
390
+ repoPath: canon,
391
+ workspaceDir: canon,
392
+ specrailsDir,
393
+ ticketsPath: path_1.default.join(specrailsDir, 'local-tickets.json'),
394
+ backlogConfigPath: path_1.default.join(specrailsDir, 'backlog-config.json'),
395
+ profilesDir: path_1.default.join(specrailsDir, 'profiles'),
396
+ pluginsStateDir: path_1.default.join(specrailsDir, 'plugins'),
397
+ fileSummariesDir: path_1.default.join(specrailsDir, 'file-summaries'),
398
+ stateDir: path_1.default.join(canon, '.claude'),
399
+ };
400
+ }
401
+ return {
402
+ isLegacy: false,
403
+ repoPath: entry.codeRoot || canon,
404
+ workspaceDir: entry.workspaceDir,
405
+ specrailsDir: path_1.default.join(entry.workspaceDir, '.specrails'),
406
+ ticketsPath: entry.ticketsPath,
407
+ backlogConfigPath: entry.backlogConfigPath,
408
+ profilesDir: entry.profilesDir,
409
+ pluginsStateDir: entry.pluginsStateDir,
410
+ fileSummariesDir: entry.fileSummariesDir,
411
+ stateDir: entry.stateDir,
412
+ entry,
413
+ };
414
+ }
415
+ /** Delete a repo's registry entry (no-op if absent). Keyed by canonical path. */
416
+ function removeRegistryEntry(repoPath, home) {
417
+ const canon = canonicalizeRepoPath(repoPath);
418
+ const key = normalizeKey(canon);
419
+ withFileLock(home, () => {
420
+ const reg = readRegistryOrEmpty(home);
421
+ if (!reg.projects[key])
422
+ return;
423
+ delete reg.projects[key];
424
+ reg.schemaVersion = exports.REGISTRY_SCHEMA_VERSION;
425
+ reg.generator = 'specrails-desktop';
426
+ reg.updatedAt = new Date().toISOString();
427
+ atomicWrite(registryPath(home), JSON.stringify(reg, null, 2) + '\n');
428
+ });
429
+ }
430
+ /**
431
+ * MERGE-upsert one entry per desktop project, leaving entries NOT owned by
432
+ * desktop (`source:'core-standalone'` for repos desktop doesn't track)
433
+ * UNTOUCHED. Never wholesale-wipes the registry. Run once at startup to
434
+ * self-heal a hand-edited / partially-written registry, and to backfill
435
+ * entries for projects added before the mirror existed.
436
+ *
437
+ * Adoption of a core-standalone entry happens exactly as in `mirrorProjectEntry`
438
+ * when a desktop project's canonical repo path matches a core-standalone key.
439
+ *
440
+ * All upserts happen under a single lock so the file is read + rewritten once.
441
+ */
442
+ function reconcileFromProjects(projects, home) {
443
+ if (projects.length === 0)
444
+ return;
445
+ withFileLock(home, () => {
446
+ const reg = readRegistryOrEmpty(home);
447
+ const now = new Date().toISOString();
448
+ let changed = false;
449
+ for (const p of projects) {
450
+ const canon = canonicalizeRepoPath(p.repoPath);
451
+ const key = normalizeKey(canon);
452
+ const { providers, primaryProvider } = normalizeProviders(p.providers, p.primaryProvider);
453
+ const existing = reg.projects[key];
454
+ // Startup reconcile (touchInstall=false): preserve the immutable
455
+ // slug/workspace of any existing entry — never re-home an adopted or
456
+ // prior-desktop entry (that would strand on-disk artifacts every boot).
457
+ const entry = buildMirroredEntry(existing, canon, p.slug, { providers, primaryProvider, coreVersion: p.coreVersion, desktopProjectId: p.desktopProjectId }, now, false, home);
458
+ reg.projects[key] = entry;
459
+ changed = true;
460
+ }
461
+ if (!changed)
462
+ return;
463
+ reg.schemaVersion = exports.REGISTRY_SCHEMA_VERSION;
464
+ reg.generator = 'specrails-desktop';
465
+ reg.updatedAt = now;
466
+ atomicWrite(registryPath(home), JSON.stringify(reg, null, 2) + '\n');
467
+ });
468
+ }
@@ -110,9 +110,8 @@ class AttachmentManager {
110
110
  fs_1.default.mkdirSync(dir, { recursive: true });
111
111
  fs_1.default.writeFileSync(path_1.default.join(dir, storedName), opts.file.buffer);
112
112
  fs_1.default.writeFileSync(this.sidecarPath(opts.slug, opts.ticketKey, id), JSON.stringify(attachment, null, 2), 'utf-8');
113
- if (opts.projectPath) {
114
- const ticketFile = (0, ticket_store_1.resolveTicketStoragePath)(opts.projectPath);
115
- (0, ticket_store_1.mutateStore)(ticketFile, (store) => {
113
+ if (opts.ticketStorePath) {
114
+ (0, ticket_store_1.mutateStore)(opts.ticketStorePath, (store) => {
116
115
  const ticket = store.tickets[String(opts.ticketKey)];
117
116
  if (ticket) {
118
117
  ticket.attachments = [...(ticket.attachments ?? []), attachment];
@@ -160,9 +159,8 @@ class AttachmentManager {
160
159
  const side = this.sidecarPath(opts.slug, opts.ticketKey, opts.attachmentId);
161
160
  if (fs_1.default.existsSync(side))
162
161
  fs_1.default.unlinkSync(side);
163
- if (opts.projectPath) {
164
- const ticketFile = (0, ticket_store_1.resolveTicketStoragePath)(opts.projectPath);
165
- (0, ticket_store_1.mutateStore)(ticketFile, (store) => {
162
+ if (opts.ticketStorePath) {
163
+ (0, ticket_store_1.mutateStore)(opts.ticketStorePath, (store) => {
166
164
  const ticket = store.tickets[String(opts.ticketKey)];
167
165
  if (ticket?.attachments) {
168
166
  ticket.attachments = ticket.attachments.filter((a) => a.id !== opts.attachmentId);
@@ -189,8 +187,7 @@ class AttachmentManager {
189
187
  fs_1.default.mkdirSync(path_1.default.dirname(dst), { recursive: true });
190
188
  fs_1.default.renameSync(src, dst);
191
189
  const list = this.list(opts.slug, opts.realTicketId);
192
- const ticketFile = (0, ticket_store_1.resolveTicketStoragePath)(opts.projectPath);
193
- (0, ticket_store_1.mutateStore)(ticketFile, (store) => {
190
+ (0, ticket_store_1.mutateStore)(opts.ticketStorePath, (store) => {
194
191
  const ticket = store.tickets[String(opts.realTicketId)];
195
192
  if (ticket) {
196
193
  const existing = ticket.attachments ?? [];
@@ -340,7 +340,7 @@ class BrowserCaptureManager {
340
340
  const screenshot = await this.attachments.upload({
341
341
  slug: this.projectSlug,
342
342
  ticketKey: pendingSpecId,
343
- projectPath: null,
343
+ ticketStorePath: null,
344
344
  file: {
345
345
  buffer: png,
346
346
  originalname: `screen-capture-${stamp}.png`,
@@ -352,7 +352,7 @@ class BrowserCaptureManager {
352
352
  const domAttachment = await this.attachments.upload({
353
353
  slug: this.projectSlug,
354
354
  ticketKey: pendingSpecId,
355
- projectPath: null,
355
+ ticketStorePath: null,
356
356
  file: {
357
357
  buffer: domJson,
358
358
  originalname: `page-dom-${stamp}.json`,
@@ -471,7 +471,7 @@ class BrowserCaptureManager {
471
471
  const attachment = await this.attachments.upload({
472
472
  slug: this.projectSlug,
473
473
  ticketKey: pendingSpecId,
474
- projectPath: null,
474
+ ticketStorePath: null,
475
475
  file: { buffer: png, originalname: `screen-capture-${key}-${stamp}.png`, mimetype: 'image/png', size: png.length },
476
476
  });
477
477
  breakpoints[key] = { attachment, dataUrl: `data:image/png;base64,${png.toString('base64')}`, viewport: dims[key] };
@@ -484,7 +484,7 @@ class BrowserCaptureManager {
484
484
  const domAttachment = await this.attachments.upload({
485
485
  slug: this.projectSlug,
486
486
  ticketKey: pendingSpecId,
487
- projectPath: null,
487
+ ticketStorePath: null,
488
488
  file: { buffer: domJson, originalname: `page-dom-${stamp}.json`, mimetype: 'application/json', size: domJson.length },
489
489
  });
490
490
  return {
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getBundledCoreRoot = getBundledCoreRoot;
7
+ exports.getBundledCoreCli = getBundledCoreCli;
8
+ exports.getBundledCoreVersion = getBundledCoreVersion;
9
+ exports.hasBundledCore = hasBundledCore;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ /**
13
+ * Bundled specrails-core resolution.
14
+ *
15
+ * The Tauri host (src-tauri/src/lib.rs) sets `SPECRAILS_BUNDLED_CORE_PATH` to
16
+ * `<resource_dir>/core` — but ONLY when the compiled CLI actually exists on disk
17
+ * (existence-gated, mirroring `SPECRAILS_BUNDLED_RUNTIMES_PATH`). When the env is
18
+ * absent (a build that shipped no core, a partial extraction, or any non-desktop
19
+ * mode like `npm run dev:server`) these helpers return null and EVERY caller
20
+ * falls back to the legacy `npx specrails-core` path — byte-identical to today.
21
+ *
22
+ * This is the single chokepoint: do NOT read `SPECRAILS_BUNDLED_CORE_PATH`
23
+ * anywhere else; resolve through `getBundledCoreCli()` / `getBundledCoreRoot()`.
24
+ */
25
+ /** The bundled core package root (`<resource_dir>/core`) or null when absent. */
26
+ function getBundledCoreRoot() {
27
+ const p = process.env.SPECRAILS_BUNDLED_CORE_PATH;
28
+ if (!p || p.length === 0)
29
+ return null;
30
+ // Existence-gate (defence-in-depth — lib.rs already gates, but a stale env
31
+ // from a partial uninstall must never make us shell out to a missing file).
32
+ if (!fs_1.default.existsSync(p))
33
+ return null;
34
+ return p;
35
+ }
36
+ /**
37
+ * Absolute path to the bundled core CLI entry
38
+ * (`<core>/dist/installer/cli.js`), or null when no bundled core is present (→
39
+ * caller falls back to `npx specrails-core`).
40
+ */
41
+ function getBundledCoreCli() {
42
+ const root = getBundledCoreRoot();
43
+ if (!root)
44
+ return null;
45
+ const cli = path_1.default.join(root, 'dist', 'installer', 'cli.js');
46
+ if (!fs_1.default.existsSync(cli))
47
+ return null;
48
+ return cli;
49
+ }
50
+ /**
51
+ * The version of the bundled core (from `<core>/package.json`), or null when no
52
+ * bundled core / unreadable. FrameworkManager.versionCheck compares this against
53
+ * the materialized `framework/current` to decide whether to re-materialize.
54
+ */
55
+ function getBundledCoreVersion() {
56
+ const root = getBundledCoreRoot();
57
+ if (!root)
58
+ return null;
59
+ const pkgPath = path_1.default.join(root, 'package.json');
60
+ try {
61
+ const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8'));
62
+ const v = pkg.version?.trim();
63
+ return v && v.length > 0 ? v : null;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ /** True when a usable bundled core (env + CLI on disk) is present. */
70
+ function hasBundledCore() {
71
+ return getBundledCoreCli() !== null;
72
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getBundledOpenspecCli = getBundledOpenspecCli;
7
+ exports.hasBundledOpenspec = hasBundledOpenspec;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ /**
11
+ * Bundled `@fission-ai/openspec` resolution.
12
+ *
13
+ * The Tauri host (src-tauri/src/lib.rs) sets `SPECRAILS_BUNDLED_OPENSPEC_PATH` to
14
+ * `<resource_dir>/openspec` — but ONLY when the openspec CLI entry actually
15
+ * exists on disk (existence-gated, mirroring `SPECRAILS_BUNDLED_CORE_PATH`).
16
+ * When the env is absent (a build that shipped no openspec, a partial
17
+ * extraction, or any non-desktop mode like `npm run dev:server`) these helpers
18
+ * return null and the bundled-core init falls back to the legacy
19
+ * `npx @fission-ai/openspec` path — byte-identical to today.
20
+ *
21
+ * openspec is bundled as a `npm install`ed tree (it has runtime deps), so the
22
+ * CLI lives at:
23
+ * <openspec>/node_modules/@fission-ai/openspec/bin/openspec.js
24
+ *
25
+ * This is the single chokepoint: do NOT read `SPECRAILS_BUNDLED_OPENSPEC_PATH`
26
+ * anywhere else; resolve through `getBundledOpenspecCli()`.
27
+ */
28
+ /** Relative path (from the bundle root) to the openspec CLI node entry. */
29
+ const OPENSPEC_CLI_REL = path_1.default.join('node_modules', '@fission-ai', 'openspec', 'bin', 'openspec.js');
30
+ /** The bundled openspec root (`<resource_dir>/openspec`) or null when absent. */
31
+ function getBundledOpenspecRoot() {
32
+ const p = process.env.SPECRAILS_BUNDLED_OPENSPEC_PATH;
33
+ if (!p || p.length === 0)
34
+ return null;
35
+ // Existence-gate (defence-in-depth — lib.rs already gates, but a stale env
36
+ // from a partial uninstall must never make us point at a missing file).
37
+ if (!fs_1.default.existsSync(p))
38
+ return null;
39
+ return p;
40
+ }
41
+ /**
42
+ * Absolute path to the bundled openspec CLI node entry
43
+ * (`<openspec>/node_modules/@fission-ai/openspec/bin/openspec.js`), or null when
44
+ * no bundled openspec is present (→ bundled-core init falls back to npx openspec).
45
+ */
46
+ function getBundledOpenspecCli() {
47
+ const root = getBundledOpenspecRoot();
48
+ if (!root)
49
+ return null;
50
+ const cli = path_1.default.join(root, OPENSPEC_CLI_REL);
51
+ if (!fs_1.default.existsSync(cli))
52
+ return null;
53
+ return cli;
54
+ }
55
+ /** True when a usable bundled openspec (env + CLI on disk) is present. */
56
+ function hasBundledOpenspec() {
57
+ return getBundledOpenspecCli() !== null;
58
+ }