specrails-desktop 2.7.0 → 2.9.0

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 (329) hide show
  1. package/README.md +23 -19
  2. package/client/dist/assets/{ActivityFeedPage-LKqd18-G.js → ActivityFeedPage-DNqnf1fZ.js} +1 -1
  3. package/client/dist/assets/{AgentsPage-Cb-b-6Ot.js → AgentsPage-vmNIEbGM.js} +1 -1
  4. package/client/dist/assets/{AnalyticsPage-HVxQQ1wy.js → AnalyticsPage-CdfN0ofZ.js} +1 -1
  5. package/client/dist/assets/{BarChart-BOyHB0dw.js → BarChart-CIkopHjl.js} +1 -1
  6. package/client/dist/assets/{CodePage-DnOnwKGB.js → CodePage-DDRNU5FN.js} +1 -1
  7. package/client/dist/assets/{DesktopAnalyticsPage-D2auU39x.js → DesktopAnalyticsPage-Cl3sKKSG.js} +1 -1
  8. package/client/dist/assets/{DocsDialog-CTuDX3GK.js → DocsDialog-BGrBOfUr.js} +2 -2
  9. package/client/dist/assets/{DocsPage-DRyMmu0Z.js → DocsPage-CY-2SSzw.js} +2 -2
  10. package/client/dist/assets/{ExportDropdown-DO-GGiMh.js → ExportDropdown-BRHcvP0r.js} +1 -1
  11. package/client/dist/assets/{IntegrationsPage-BhbO4jFT.js → IntegrationsPage-nKdLB4Ub.js} +1 -1
  12. package/client/dist/assets/{JobDetailPage-DJooEg1s.js → JobDetailPage-Bf0A6WWQ.js} +1 -1
  13. package/client/dist/assets/{JobsPage-BbaC-YOg.js → JobsPage-Vg4nXPdL.js} +1 -1
  14. package/client/dist/assets/{dist-js-CiIVMsx3.js → dist-js-0i_klubI.js} +1 -1
  15. package/client/dist/assets/{dist-js-Xc2lRKp2.js → dist-js-CUs5GjwA.js} +1 -1
  16. package/client/dist/assets/{index-DK214dak.js → index-BXoHFtfG.js} +8 -8
  17. package/client/dist/assets/index-D6BaYRRU.css +2 -0
  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-D6M_MvoC.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/{useProjectCache-DVNypkmR.js → useProjectCache-BeyBSNpD.js} +1 -1
  36. package/client/dist/index.html +4 -4
  37. package/docs/README.md +5 -2
  38. package/docs/agy-cli-provider-study.md +78 -0
  39. package/docs/cli.md +23 -4
  40. package/docs/codex.md +116 -58
  41. package/docs/creating-specs.md +19 -5
  42. package/docs/customizing.md +27 -6
  43. package/docs/gemini.md +225 -73
  44. package/docs/getting-started.md +18 -9
  45. package/docs/guide/de/agents/1-meet-the-agents.md +38 -0
  46. package/docs/guide/de/agents/2-profiles-and-the-balanced-default.md +45 -0
  47. package/docs/guide/de/agents/3-customizing-models-per-agent.md +60 -0
  48. package/docs/guide/de/agents/4-custom-agents-catalog.md +43 -0
  49. package/docs/guide/de/getting-started/1-what-is-specrails.md +49 -0
  50. package/docs/guide/de/getting-started/2-installing-and-first-run.md +42 -0
  51. package/docs/guide/de/getting-started/3-adding-your-first-project.md +58 -0
  52. package/docs/guide/de/getting-started/4-the-dashboard-tour.md +53 -0
  53. package/docs/guide/de/insights/1-analytics-and-cost-tracking.md +78 -0
  54. package/docs/guide/de/insights/2-the-integrated-terminal.md +46 -0
  55. package/docs/guide/de/insights/3-code-explorer.md +50 -0
  56. package/docs/guide/de/integrations/1-ai-providers.md +64 -0
  57. package/docs/guide/de/integrations/2-plugins.md +44 -0
  58. package/docs/guide/de/integrations/3-jira-integration.md +71 -0
  59. package/docs/guide/de/integrations/4-mobile-companion.md +38 -0
  60. package/docs/guide/de/pipeline/1-rails-and-jobs.md +94 -0
  61. package/docs/guide/de/pipeline/2-the-job-detail-view.md +90 -0
  62. package/docs/guide/de/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  63. package/docs/guide/de/pipeline/4-picking-an-engine-per-rail.md +60 -0
  64. package/docs/guide/de/settings/1-themes.md +37 -0
  65. package/docs/guide/de/settings/2-language.md +39 -0
  66. package/docs/guide/de/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  67. package/docs/guide/de/settings/4-where-your-data-lives.md +48 -0
  68. package/docs/guide/de/specs/1-specs-and-the-backlog.md +52 -0
  69. package/docs/guide/de/specs/2-add-spec-quick-mode.md +45 -0
  70. package/docs/guide/de/specs/3-add-spec-explore-mode.md +68 -0
  71. package/docs/guide/de/specs/4-drafts-and-contract-layer.md +81 -0
  72. package/docs/guide/en/agents/1-meet-the-agents.md +38 -0
  73. package/docs/guide/en/agents/2-profiles-and-the-balanced-default.md +45 -0
  74. package/docs/guide/en/agents/3-customizing-models-per-agent.md +60 -0
  75. package/docs/guide/en/agents/4-custom-agents-catalog.md +43 -0
  76. package/docs/guide/en/getting-started/1-what-is-specrails.md +49 -0
  77. package/docs/guide/en/getting-started/2-installing-and-first-run.md +42 -0
  78. package/docs/guide/en/getting-started/3-adding-your-first-project.md +58 -0
  79. package/docs/guide/en/getting-started/4-the-dashboard-tour.md +53 -0
  80. package/docs/guide/en/insights/1-analytics-and-cost-tracking.md +78 -0
  81. package/docs/guide/en/insights/2-the-integrated-terminal.md +46 -0
  82. package/docs/guide/en/insights/3-code-explorer.md +50 -0
  83. package/docs/guide/en/integrations/1-ai-providers.md +64 -0
  84. package/docs/guide/en/integrations/2-plugins.md +44 -0
  85. package/docs/guide/en/integrations/3-jira-integration.md +71 -0
  86. package/docs/guide/en/integrations/4-mobile-companion.md +38 -0
  87. package/docs/guide/en/pipeline/1-rails-and-jobs.md +94 -0
  88. package/docs/guide/en/pipeline/2-the-job-detail-view.md +90 -0
  89. package/docs/guide/en/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  90. package/docs/guide/en/pipeline/4-picking-an-engine-per-rail.md +60 -0
  91. package/docs/guide/en/settings/1-themes.md +37 -0
  92. package/docs/guide/en/settings/2-language.md +39 -0
  93. package/docs/guide/en/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  94. package/docs/guide/en/settings/4-where-your-data-lives.md +48 -0
  95. package/docs/guide/en/specs/1-specs-and-the-backlog.md +52 -0
  96. package/docs/guide/en/specs/2-add-spec-quick-mode.md +45 -0
  97. package/docs/guide/en/specs/3-add-spec-explore-mode.md +68 -0
  98. package/docs/guide/en/specs/4-drafts-and-contract-layer.md +81 -0
  99. package/docs/guide/es/agents/1-meet-the-agents.md +38 -0
  100. package/docs/guide/es/agents/2-profiles-and-the-balanced-default.md +45 -0
  101. package/docs/guide/es/agents/3-customizing-models-per-agent.md +60 -0
  102. package/docs/guide/es/agents/4-custom-agents-catalog.md +43 -0
  103. package/docs/guide/es/getting-started/1-what-is-specrails.md +49 -0
  104. package/docs/guide/es/getting-started/2-installing-and-first-run.md +42 -0
  105. package/docs/guide/es/getting-started/3-adding-your-first-project.md +58 -0
  106. package/docs/guide/es/getting-started/4-the-dashboard-tour.md +53 -0
  107. package/docs/guide/es/insights/1-analytics-and-cost-tracking.md +78 -0
  108. package/docs/guide/es/insights/2-the-integrated-terminal.md +46 -0
  109. package/docs/guide/es/insights/3-code-explorer.md +50 -0
  110. package/docs/guide/es/integrations/1-ai-providers.md +64 -0
  111. package/docs/guide/es/integrations/2-plugins.md +44 -0
  112. package/docs/guide/es/integrations/3-jira-integration.md +71 -0
  113. package/docs/guide/es/integrations/4-mobile-companion.md +38 -0
  114. package/docs/guide/es/pipeline/1-rails-and-jobs.md +94 -0
  115. package/docs/guide/es/pipeline/2-the-job-detail-view.md +90 -0
  116. package/docs/guide/es/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  117. package/docs/guide/es/pipeline/4-picking-an-engine-per-rail.md +60 -0
  118. package/docs/guide/es/settings/1-themes.md +37 -0
  119. package/docs/guide/es/settings/2-language.md +39 -0
  120. package/docs/guide/es/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  121. package/docs/guide/es/settings/4-where-your-data-lives.md +48 -0
  122. package/docs/guide/es/specs/1-specs-and-the-backlog.md +52 -0
  123. package/docs/guide/es/specs/2-add-spec-quick-mode.md +45 -0
  124. package/docs/guide/es/specs/3-add-spec-explore-mode.md +68 -0
  125. package/docs/guide/es/specs/4-drafts-and-contract-layer.md +81 -0
  126. package/docs/guide/fr/agents/1-meet-the-agents.md +38 -0
  127. package/docs/guide/fr/agents/2-profiles-and-the-balanced-default.md +45 -0
  128. package/docs/guide/fr/agents/3-customizing-models-per-agent.md +60 -0
  129. package/docs/guide/fr/agents/4-custom-agents-catalog.md +43 -0
  130. package/docs/guide/fr/getting-started/1-what-is-specrails.md +49 -0
  131. package/docs/guide/fr/getting-started/2-installing-and-first-run.md +42 -0
  132. package/docs/guide/fr/getting-started/3-adding-your-first-project.md +58 -0
  133. package/docs/guide/fr/getting-started/4-the-dashboard-tour.md +53 -0
  134. package/docs/guide/fr/insights/1-analytics-and-cost-tracking.md +78 -0
  135. package/docs/guide/fr/insights/2-the-integrated-terminal.md +46 -0
  136. package/docs/guide/fr/insights/3-code-explorer.md +50 -0
  137. package/docs/guide/fr/integrations/1-ai-providers.md +64 -0
  138. package/docs/guide/fr/integrations/2-plugins.md +44 -0
  139. package/docs/guide/fr/integrations/3-jira-integration.md +71 -0
  140. package/docs/guide/fr/integrations/4-mobile-companion.md +38 -0
  141. package/docs/guide/fr/pipeline/1-rails-and-jobs.md +94 -0
  142. package/docs/guide/fr/pipeline/2-the-job-detail-view.md +90 -0
  143. package/docs/guide/fr/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  144. package/docs/guide/fr/pipeline/4-picking-an-engine-per-rail.md +60 -0
  145. package/docs/guide/fr/settings/1-themes.md +37 -0
  146. package/docs/guide/fr/settings/2-language.md +39 -0
  147. package/docs/guide/fr/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  148. package/docs/guide/fr/settings/4-where-your-data-lives.md +48 -0
  149. package/docs/guide/fr/specs/1-specs-and-the-backlog.md +52 -0
  150. package/docs/guide/fr/specs/2-add-spec-quick-mode.md +45 -0
  151. package/docs/guide/fr/specs/3-add-spec-explore-mode.md +68 -0
  152. package/docs/guide/fr/specs/4-drafts-and-contract-layer.md +81 -0
  153. package/docs/guide/it/agents/1-meet-the-agents.md +38 -0
  154. package/docs/guide/it/agents/2-profiles-and-the-balanced-default.md +45 -0
  155. package/docs/guide/it/agents/3-customizing-models-per-agent.md +60 -0
  156. package/docs/guide/it/agents/4-custom-agents-catalog.md +43 -0
  157. package/docs/guide/it/getting-started/1-what-is-specrails.md +49 -0
  158. package/docs/guide/it/getting-started/2-installing-and-first-run.md +42 -0
  159. package/docs/guide/it/getting-started/3-adding-your-first-project.md +58 -0
  160. package/docs/guide/it/getting-started/4-the-dashboard-tour.md +53 -0
  161. package/docs/guide/it/insights/1-analytics-and-cost-tracking.md +78 -0
  162. package/docs/guide/it/insights/2-the-integrated-terminal.md +46 -0
  163. package/docs/guide/it/insights/3-code-explorer.md +50 -0
  164. package/docs/guide/it/integrations/1-ai-providers.md +64 -0
  165. package/docs/guide/it/integrations/2-plugins.md +44 -0
  166. package/docs/guide/it/integrations/3-jira-integration.md +71 -0
  167. package/docs/guide/it/integrations/4-mobile-companion.md +38 -0
  168. package/docs/guide/it/pipeline/1-rails-and-jobs.md +94 -0
  169. package/docs/guide/it/pipeline/2-the-job-detail-view.md +90 -0
  170. package/docs/guide/it/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  171. package/docs/guide/it/pipeline/4-picking-an-engine-per-rail.md +60 -0
  172. package/docs/guide/it/settings/1-themes.md +37 -0
  173. package/docs/guide/it/settings/2-language.md +39 -0
  174. package/docs/guide/it/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  175. package/docs/guide/it/settings/4-where-your-data-lives.md +48 -0
  176. package/docs/guide/it/specs/1-specs-and-the-backlog.md +52 -0
  177. package/docs/guide/it/specs/2-add-spec-quick-mode.md +45 -0
  178. package/docs/guide/it/specs/3-add-spec-explore-mode.md +68 -0
  179. package/docs/guide/it/specs/4-drafts-and-contract-layer.md +81 -0
  180. package/docs/guide/ja/agents/1-meet-the-agents.md +38 -0
  181. package/docs/guide/ja/agents/2-profiles-and-the-balanced-default.md +45 -0
  182. package/docs/guide/ja/agents/3-customizing-models-per-agent.md +60 -0
  183. package/docs/guide/ja/agents/4-custom-agents-catalog.md +43 -0
  184. package/docs/guide/ja/getting-started/1-what-is-specrails.md +49 -0
  185. package/docs/guide/ja/getting-started/2-installing-and-first-run.md +42 -0
  186. package/docs/guide/ja/getting-started/3-adding-your-first-project.md +58 -0
  187. package/docs/guide/ja/getting-started/4-the-dashboard-tour.md +53 -0
  188. package/docs/guide/ja/insights/1-analytics-and-cost-tracking.md +78 -0
  189. package/docs/guide/ja/insights/2-the-integrated-terminal.md +46 -0
  190. package/docs/guide/ja/insights/3-code-explorer.md +50 -0
  191. package/docs/guide/ja/integrations/1-ai-providers.md +64 -0
  192. package/docs/guide/ja/integrations/2-plugins.md +44 -0
  193. package/docs/guide/ja/integrations/3-jira-integration.md +71 -0
  194. package/docs/guide/ja/integrations/4-mobile-companion.md +38 -0
  195. package/docs/guide/ja/pipeline/1-rails-and-jobs.md +94 -0
  196. package/docs/guide/ja/pipeline/2-the-job-detail-view.md +90 -0
  197. package/docs/guide/ja/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  198. package/docs/guide/ja/pipeline/4-picking-an-engine-per-rail.md +60 -0
  199. package/docs/guide/ja/settings/1-themes.md +37 -0
  200. package/docs/guide/ja/settings/2-language.md +39 -0
  201. package/docs/guide/ja/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  202. package/docs/guide/ja/settings/4-where-your-data-lives.md +48 -0
  203. package/docs/guide/ja/specs/1-specs-and-the-backlog.md +52 -0
  204. package/docs/guide/ja/specs/2-add-spec-quick-mode.md +45 -0
  205. package/docs/guide/ja/specs/3-add-spec-explore-mode.md +68 -0
  206. package/docs/guide/ja/specs/4-drafts-and-contract-layer.md +81 -0
  207. package/docs/guide/pt/agents/1-meet-the-agents.md +38 -0
  208. package/docs/guide/pt/agents/2-profiles-and-the-balanced-default.md +45 -0
  209. package/docs/guide/pt/agents/3-customizing-models-per-agent.md +60 -0
  210. package/docs/guide/pt/agents/4-custom-agents-catalog.md +43 -0
  211. package/docs/guide/pt/getting-started/1-what-is-specrails.md +49 -0
  212. package/docs/guide/pt/getting-started/2-installing-and-first-run.md +42 -0
  213. package/docs/guide/pt/getting-started/3-adding-your-first-project.md +58 -0
  214. package/docs/guide/pt/getting-started/4-the-dashboard-tour.md +53 -0
  215. package/docs/guide/pt/insights/1-analytics-and-cost-tracking.md +78 -0
  216. package/docs/guide/pt/insights/2-the-integrated-terminal.md +46 -0
  217. package/docs/guide/pt/insights/3-code-explorer.md +50 -0
  218. package/docs/guide/pt/integrations/1-ai-providers.md +64 -0
  219. package/docs/guide/pt/integrations/2-plugins.md +44 -0
  220. package/docs/guide/pt/integrations/3-jira-integration.md +71 -0
  221. package/docs/guide/pt/integrations/4-mobile-companion.md +38 -0
  222. package/docs/guide/pt/pipeline/1-rails-and-jobs.md +94 -0
  223. package/docs/guide/pt/pipeline/2-the-job-detail-view.md +90 -0
  224. package/docs/guide/pt/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  225. package/docs/guide/pt/pipeline/4-picking-an-engine-per-rail.md +60 -0
  226. package/docs/guide/pt/settings/1-themes.md +37 -0
  227. package/docs/guide/pt/settings/2-language.md +39 -0
  228. package/docs/guide/pt/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  229. package/docs/guide/pt/settings/4-where-your-data-lives.md +48 -0
  230. package/docs/guide/pt/specs/1-specs-and-the-backlog.md +52 -0
  231. package/docs/guide/pt/specs/2-add-spec-quick-mode.md +45 -0
  232. package/docs/guide/pt/specs/3-add-spec-explore-mode.md +68 -0
  233. package/docs/guide/pt/specs/4-drafts-and-contract-layer.md +81 -0
  234. package/docs/guide/zh/agents/1-meet-the-agents.md +38 -0
  235. package/docs/guide/zh/agents/2-profiles-and-the-balanced-default.md +45 -0
  236. package/docs/guide/zh/agents/3-customizing-models-per-agent.md +60 -0
  237. package/docs/guide/zh/agents/4-custom-agents-catalog.md +43 -0
  238. package/docs/guide/zh/getting-started/1-what-is-specrails.md +49 -0
  239. package/docs/guide/zh/getting-started/2-installing-and-first-run.md +42 -0
  240. package/docs/guide/zh/getting-started/3-adding-your-first-project.md +58 -0
  241. package/docs/guide/zh/getting-started/4-the-dashboard-tour.md +53 -0
  242. package/docs/guide/zh/insights/1-analytics-and-cost-tracking.md +78 -0
  243. package/docs/guide/zh/insights/2-the-integrated-terminal.md +46 -0
  244. package/docs/guide/zh/insights/3-code-explorer.md +50 -0
  245. package/docs/guide/zh/integrations/1-ai-providers.md +64 -0
  246. package/docs/guide/zh/integrations/2-plugins.md +44 -0
  247. package/docs/guide/zh/integrations/3-jira-integration.md +71 -0
  248. package/docs/guide/zh/integrations/4-mobile-companion.md +38 -0
  249. package/docs/guide/zh/pipeline/1-rails-and-jobs.md +94 -0
  250. package/docs/guide/zh/pipeline/2-the-job-detail-view.md +90 -0
  251. package/docs/guide/zh/pipeline/3-batch-implement-and-multi-feature.md +78 -0
  252. package/docs/guide/zh/pipeline/4-picking-an-engine-per-rail.md +60 -0
  253. package/docs/guide/zh/settings/1-themes.md +37 -0
  254. package/docs/guide/zh/settings/2-language.md +39 -0
  255. package/docs/guide/zh/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
  256. package/docs/guide/zh/settings/4-where-your-data-lives.md +48 -0
  257. package/docs/guide/zh/specs/1-specs-and-the-backlog.md +52 -0
  258. package/docs/guide/zh/specs/2-add-spec-quick-mode.md +45 -0
  259. package/docs/guide/zh/specs/3-add-spec-explore-mode.md +68 -0
  260. package/docs/guide/zh/specs/4-drafts-and-contract-layer.md +81 -0
  261. package/docs/internals/README.md +1 -1
  262. package/docs/internals/adding-a-provider.md +192 -59
  263. package/docs/internals/api-reference.md +130 -21
  264. package/docs/internals/architecture.md +22 -9
  265. package/docs/internals/bundled-framework-build-plan.md +264 -0
  266. package/docs/internals/configuration.md +33 -8
  267. package/docs/internals/global-artifacts-alignment-contract.md +486 -0
  268. package/docs/internals/global-artifacts-relocation-evaluation.md +294 -0
  269. package/docs/internals/operations-runbook.md +16 -5
  270. package/docs/internals/profiles.md +42 -14
  271. package/docs/platforms/macos.md +27 -8
  272. package/docs/platforms/windows.md +20 -6
  273. package/docs/running-pipelines.md +17 -9
  274. package/docs/terminal.md +9 -3
  275. package/docs/tracking-cost.md +17 -11
  276. package/package.json +1 -1
  277. package/server/dist/agent-refine-manager.js +20 -5
  278. package/server/dist/artifact-registry.js +468 -0
  279. package/server/dist/attachment-manager.js +5 -8
  280. package/server/dist/browser-capture-manager.js +4 -4
  281. package/server/dist/bundled-core.js +72 -0
  282. package/server/dist/bundled-openspec.js +58 -0
  283. package/server/dist/chat-manager.js +42 -5
  284. package/server/dist/code-explorer-router.js +10 -7
  285. package/server/dist/config.js +7 -2
  286. package/server/dist/context-budget.js +17 -6
  287. package/server/dist/context-scope.js +6 -2
  288. package/server/dist/contract-refine-runner.js +31 -9
  289. package/server/dist/desktop-router.js +39 -14
  290. package/server/dist/docs-router.js +210 -132
  291. package/server/dist/file-summary-manager.js +41 -16
  292. package/server/dist/framework-manager.js +248 -0
  293. package/server/dist/framework-migration.js +308 -0
  294. package/server/dist/index.js +30 -0
  295. package/server/dist/install-config-path.js +73 -0
  296. package/server/dist/jira/jira-sync-manager.js +23 -11
  297. package/server/dist/openspec-shim.js +153 -0
  298. package/server/dist/plugins-router.js +19 -8
  299. package/server/dist/profiles-router.js +38 -16
  300. package/server/dist/project-registry.js +101 -3
  301. package/server/dist/project-router-chat.js +1 -1
  302. package/server/dist/project-router-helpers.js +25 -12
  303. package/server/dist/project-router-jobs.js +3 -3
  304. package/server/dist/project-router-settings.js +8 -6
  305. package/server/dist/project-router-setup.js +27 -10
  306. package/server/dist/project-router-spending.js +6 -1
  307. package/server/dist/project-router-tickets.js +30 -10
  308. package/server/dist/project-router.js +16 -1
  309. package/server/dist/providers/gemini-adapter.js +4 -0
  310. package/server/dist/providers/gemini-agent-ack.js +65 -0
  311. package/server/dist/queue-manager.js +156 -12
  312. package/server/dist/setup-manager.js +131 -29
  313. package/server/dist/smash-runner.js +21 -6
  314. package/server/dist/ticket-store.js +6 -2
  315. package/server/dist/ticket-watcher.js +5 -1
  316. package/server/dist/util/stream-display.js +18 -3
  317. package/server/dist/vitest-setup.js +25 -0
  318. package/server/dist/workspace-manager.js +199 -0
  319. package/server/dist/workspace-resolution.js +147 -0
  320. package/client/dist/assets/index-DgKfQFcf.css +0 -2
  321. package/client/dist/assets/setup-BIIkb-_K.js +0 -1
  322. package/client/dist/assets/setup-BeQxu9kD.js +0 -1
  323. package/client/dist/assets/setup-CPa6GnlI.js +0 -1
  324. package/client/dist/assets/setup-CZl4OEJx.js +0 -1
  325. package/client/dist/assets/setup-ChpodNfn.js +0 -1
  326. package/client/dist/assets/setup-D_fjJH6u.js +0 -1
  327. package/client/dist/assets/setup-YzD8DX4O.js +0 -1
  328. package/client/dist/assets/setup-fRpDozmq.js +0 -1
  329. package/docs/adding-a-provider.md +0 -107
@@ -0,0 +1,248 @@
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.atomicWrite = exports.FrameworkManager = void 0;
7
+ exports.frameworkRoot = frameworkRoot;
8
+ exports.readCurrentFrameworkVersion = readCurrentFrameworkVersion;
9
+ const child_process_1 = require("child_process");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const bundled_core_1 = require("./bundled-core");
13
+ const artifact_registry_1 = require("./artifact-registry");
14
+ Object.defineProperty(exports, "atomicWrite", { enumerable: true, get: function () { return artifact_registry_1.atomicWrite; } });
15
+ /** `~/.specrails/framework` — same home as the registry. */
16
+ function frameworkRoot(home) {
17
+ return path_1.default.join((0, artifact_registry_1.resolveHome)(home), '.specrails', 'framework');
18
+ }
19
+ /** Path to the `current` symlink under the framework root. */
20
+ function currentLink(home) {
21
+ return path_1.default.join(frameworkRoot(home), 'current');
22
+ }
23
+ /**
24
+ * The version `framework/current` resolves to, or null when `current` is absent
25
+ * / not a `<version>` dir. Reads the basename of the symlink target.
26
+ */
27
+ function readCurrentFrameworkVersion(home) {
28
+ const link = currentLink(home);
29
+ try {
30
+ const st = fs_1.default.lstatSync(link);
31
+ let target;
32
+ if (st.isSymbolicLink()) {
33
+ target = fs_1.default.readlinkSync(link);
34
+ }
35
+ else if (st.isDirectory()) {
36
+ // Windows copy fallback: `current` is a real dir holding a marker.
37
+ target = link;
38
+ }
39
+ else {
40
+ return null;
41
+ }
42
+ const resolved = path_1.default.isAbsolute(target) ? target : path_1.default.resolve(frameworkRoot(home), target);
43
+ const base = path_1.default.basename(resolved);
44
+ return base === 'current' ? null : base;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ class FrameworkManager {
51
+ home;
52
+ broadcast;
53
+ constructor(opts = {}) {
54
+ this.home = opts.home;
55
+ this.broadcast = opts.broadcast;
56
+ }
57
+ /** True when a usable bundled core is present (else all methods no-op). */
58
+ isAvailable() {
59
+ return (0, bundled_core_1.getBundledCoreCli)() !== null;
60
+ }
61
+ /** The bundled core version (the version the app should materialize), or null. */
62
+ bundledVersion() {
63
+ return (0, bundled_core_1.getBundledCoreVersion)();
64
+ }
65
+ /**
66
+ * Materialize the framework for `version` (default: the bundled core version)
67
+ * for each provider — idempotent + offline. Shells out to the bundled core
68
+ * `install-framework --no-swap` once per provider, so the materialize step
69
+ * NEVER moves `current`: the caller (`versionCheck`) materializes EVERY
70
+ * provider first, then issues a SINGLE `swapCurrent(version)` only when all of
71
+ * them succeeded. This prevents a half-installed multi-provider release from
72
+ * flipping `current` to a version whose other providerDirs are missing. No-ops
73
+ * (ran:false) when no bundled core is present.
74
+ */
75
+ materialize(version, providers = ['claude']) {
76
+ const cli = (0, bundled_core_1.getBundledCoreCli)();
77
+ if (!cli) {
78
+ return { ran: false, version: null, providers: [], errors: [] };
79
+ }
80
+ const ver = version ?? this.bundledVersion();
81
+ if (!ver) {
82
+ return { ran: false, version: null, providers: [], errors: [] };
83
+ }
84
+ const fwDir = frameworkRoot(this.home);
85
+ fs_1.default.mkdirSync(fwDir, { recursive: true });
86
+ const errors = [];
87
+ const done = [];
88
+ for (const provider of dedupe(providers)) {
89
+ const res = (0, child_process_1.spawnSync)(process.execPath, [
90
+ cli,
91
+ 'install-framework',
92
+ '--framework-dir',
93
+ fwDir,
94
+ '--provider',
95
+ provider,
96
+ '--version',
97
+ ver,
98
+ // Materialize-all-then-swap-once: each provider is installed WITHOUT
99
+ // moving `current`; the finalising swap happens in versionCheck.
100
+ '--no-swap',
101
+ ], { env: this.childEnv(), encoding: 'utf-8', timeout: 120_000 });
102
+ if (res.error || (res.status ?? 1) !== 0) {
103
+ const msg = res.error?.message ?? (res.stderr || `exit ${res.status ?? 'unknown'}`);
104
+ errors.push({ provider, message: `${msg}`.trim() });
105
+ }
106
+ else {
107
+ done.push(provider);
108
+ }
109
+ }
110
+ return { ran: true, version: ver, providers: done, errors };
111
+ }
112
+ /**
113
+ * Atomically point `framework/current` at `version` via core's dedicated
114
+ * `swap-current` subcommand — the ONE finalising step after every provider was
115
+ * materialized with `--no-swap`. Guard/no-op when `current` already points at
116
+ * `version`. Returns true when `current` ended up at `version`.
117
+ *
118
+ * The `provider` param is retained for signature compatibility but is unused:
119
+ * `swap-current` only moves the version pointer (provider-invariant).
120
+ */
121
+ swapCurrent(version, _provider = 'claude') {
122
+ const cli = (0, bundled_core_1.getBundledCoreCli)();
123
+ if (!cli)
124
+ return false;
125
+ if (readCurrentFrameworkVersion(this.home) === version)
126
+ return true;
127
+ const fwDir = frameworkRoot(this.home);
128
+ const res = (0, child_process_1.spawnSync)(process.execPath, [cli, 'swap-current', '--framework-dir', fwDir, '--version', version], { env: this.childEnv(), encoding: 'utf-8', timeout: 120_000 });
129
+ if (res.error || (res.status ?? 1) !== 0)
130
+ return false;
131
+ return readCurrentFrameworkVersion(this.home) === version;
132
+ }
133
+ /**
134
+ * Compare the bundled core version with `framework/current`. When they differ
135
+ * (or current is absent), materialize the bundled version + swap `current` and
136
+ * broadcast `framework.updated`. Called on first-run + post-update. No-ops when
137
+ * no bundled core is present. Returns the resulting current version (or null).
138
+ */
139
+ versionCheck(providers = ['claude']) {
140
+ const bundled = this.bundledVersion();
141
+ if (!(0, bundled_core_1.getBundledCoreCli)() || !bundled) {
142
+ return { swapped: false, version: readCurrentFrameworkVersion(this.home) };
143
+ }
144
+ const current = readCurrentFrameworkVersion(this.home);
145
+ if (current === bundled) {
146
+ return { swapped: false, version: current };
147
+ }
148
+ // Materialize EVERY requested provider first (each with `--no-swap`), then
149
+ // swap `current` ONCE — and ONLY when every provider materialized cleanly.
150
+ // A partial multi-provider install must NEVER flip `current`: doing so would
151
+ // point `current` at a version whose other providerDirs are missing, so a
152
+ // rail spawning under a non-materialized provider would fail to assemble.
153
+ const mat = this.materialize(bundled, providers);
154
+ if (!mat.ran) {
155
+ return { swapped: false, version: current };
156
+ }
157
+ if (mat.errors.length > 0) {
158
+ // Do NOT swap; surface the failure (don't discard it, don't report success).
159
+ try {
160
+ this.broadcast?.({
161
+ type: 'framework.update_failed',
162
+ version: bundled,
163
+ errors: mat.errors,
164
+ });
165
+ }
166
+ catch {
167
+ /* broadcast is best-effort */
168
+ }
169
+ return { swapped: false, version: current };
170
+ }
171
+ if (mat.providers.length === 0) {
172
+ // No errors yet nothing materialized (empty/duplicate-only provider list).
173
+ return { swapped: false, version: current };
174
+ }
175
+ const ok = this.swapCurrent(bundled);
176
+ if (ok) {
177
+ try {
178
+ this.broadcast?.({ type: 'framework.updated', version: bundled });
179
+ }
180
+ catch {
181
+ /* broadcast is best-effort */
182
+ }
183
+ return { swapped: true, version: bundled };
184
+ }
185
+ // Materialized cleanly but the swap itself failed — surface as a failure.
186
+ try {
187
+ this.broadcast?.({
188
+ type: 'framework.update_failed',
189
+ version: bundled,
190
+ errors: [{ provider: providers[0] ?? 'claude', message: 'swap-current failed' }],
191
+ });
192
+ }
193
+ catch {
194
+ /* broadcast is best-effort */
195
+ }
196
+ return { swapped: false, version: readCurrentFrameworkVersion(this.home) };
197
+ }
198
+ /**
199
+ * Assemble a project workspace by SYMLINKING the materialized framework into
200
+ * it (offline) via the bundled core `assemble` subcommand. Returns false (no-op)
201
+ * when no bundled core is present — caller falls back to legacy npx assembly.
202
+ */
203
+ assembleWorkspace(input) {
204
+ const cli = (0, bundled_core_1.getBundledCoreCli)();
205
+ if (!cli)
206
+ return { ran: false };
207
+ const ver = input.version ?? this.bundledVersion();
208
+ if (!ver)
209
+ return { ran: false };
210
+ const fwDir = frameworkRoot(this.home);
211
+ const res = (0, child_process_1.spawnSync)(process.execPath, [
212
+ cli,
213
+ 'assemble',
214
+ '--workspace',
215
+ input.workspace,
216
+ '--framework-dir',
217
+ fwDir,
218
+ '--provider',
219
+ input.provider,
220
+ '--version',
221
+ ver,
222
+ '--code-root',
223
+ input.codeRoot,
224
+ ], { env: this.childEnv(), encoding: 'utf-8', timeout: 120_000 });
225
+ if (res.error || (res.status ?? 1) !== 0) {
226
+ const msg = res.error?.message ?? (res.stderr || `exit ${res.status ?? 'unknown'}`);
227
+ return { ran: true, error: `${msg}`.trim() };
228
+ }
229
+ return { ran: true };
230
+ }
231
+ /** Child env: thread the registry home so the core CLI co-locates framework + registry. */
232
+ childEnv() {
233
+ const env = { ...process.env };
234
+ if (this.home) {
235
+ env.SPECRAILS_REGISTRY_HOME = this.home;
236
+ }
237
+ // Point the bundled core CLI's scriptDir at the bundled package so its
238
+ // template/command sources resolve from the bundle, not a global install.
239
+ const coreRoot = process.env.SPECRAILS_BUNDLED_CORE_PATH;
240
+ if (coreRoot)
241
+ env.SPECRAILS_CORE_SCRIPT_DIR = coreRoot;
242
+ return env;
243
+ }
244
+ }
245
+ exports.FrameworkManager = FrameworkManager;
246
+ function dedupe(values) {
247
+ return Array.from(new Set(values));
248
+ }
@@ -0,0 +1,308 @@
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.providerDirFor = providerDirFor;
7
+ exports.migrateWorkspaceToSymlinks = migrateWorkspaceToSymlinks;
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const framework_manager_1 = require("./framework-manager");
12
+ const workspace_manager_1 = require("./workspace-manager");
13
+ const artifact_registry_1 = require("./artifact-registry");
14
+ /** The shared, statically-linked subtrees of a providerDir. `agent-memory` is
15
+ * intentionally absent — it is real per-workspace state, never compared/linked. */
16
+ const SHARED_SUBTREES = ['commands', 'agents', 'skills', 'rules'];
17
+ /** Map a provider id to its providerDir name (mirrors core). */
18
+ function providerDirFor(provider) {
19
+ if (provider === 'codex')
20
+ return '.codex';
21
+ if (provider === 'gemini')
22
+ return '.gemini';
23
+ return '.claude';
24
+ }
25
+ /** sha256 of a file's bytes, or null when unreadable. */
26
+ function hashFile(p) {
27
+ try {
28
+ return crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(p)).digest('hex');
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /** True when `p` is a symbolic link. */
35
+ function isSymlink(p) {
36
+ try {
37
+ return fs_1.default.lstatSync(p).isSymbolicLink();
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ /** True when `p` is a real (non-symlink) directory. */
44
+ function isRealDir(p) {
45
+ try {
46
+ const st = fs_1.default.lstatSync(p);
47
+ return st.isDirectory() && !st.isSymbolicLink();
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ /** Recursively list relative file paths under `dir` (dirs flattened to files). */
54
+ function listFilesRel(dir, base = dir) {
55
+ const out = [];
56
+ let entries;
57
+ try {
58
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
59
+ }
60
+ catch {
61
+ return out;
62
+ }
63
+ for (const e of entries) {
64
+ const abs = path_1.default.join(dir, e.name);
65
+ if (e.isSymbolicLink())
66
+ continue; // a symlink inside a copy is not a real file to compare
67
+ if (e.isDirectory()) {
68
+ out.push(...listFilesRel(abs, base));
69
+ }
70
+ else if (e.isFile()) {
71
+ out.push(path_1.default.relative(base, abs));
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ /** A file is a user/plugin custom agent (expected, never part of the framework). */
77
+ function isCustomAgent(rel) {
78
+ return path_1.default.basename(rel).startsWith('custom-');
79
+ }
80
+ /**
81
+ * Compare the workspace providerDir's shared subtrees against
82
+ * `framework/current/<providerDir>/...`. A file present in the copy that differs
83
+ * from (or is missing in) `current` — excluding `custom-*` agents — counts as
84
+ * local divergence.
85
+ */
86
+ function compareSharedSubtrees(workspaceProviderDir, fwProviderDir) {
87
+ let hadSharedSubtree = false;
88
+ for (const sub of SHARED_SUBTREES) {
89
+ const copyDir = path_1.default.join(workspaceProviderDir, sub);
90
+ if (!isRealDir(copyDir))
91
+ continue;
92
+ hadSharedSubtree = true;
93
+ const fwSub = path_1.default.join(fwProviderDir, sub);
94
+ for (const rel of listFilesRel(copyDir)) {
95
+ if (sub === 'agents' && isCustomAgent(rel))
96
+ continue; // expected, ignore
97
+ const copyHash = hashFile(path_1.default.join(copyDir, rel));
98
+ const fwHash = hashFile(path_1.default.join(fwSub, rel));
99
+ if (copyHash === null || fwHash === null || copyHash !== fwHash) {
100
+ return { matches: false, diverged: path_1.default.join(sub, rel), hadSharedSubtree };
101
+ }
102
+ }
103
+ }
104
+ return { matches: hadSharedSubtree, hadSharedSubtree };
105
+ }
106
+ /** Resolve a symlink target absolutely (or null). */
107
+ function readlinkAbs(p) {
108
+ try {
109
+ const t = fs_1.default.readlinkSync(p);
110
+ return path_1.default.isAbsolute(t) ? t : path_1.default.resolve(path_1.default.dirname(p), t);
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ /** True when `linkPath` resolves into `<frameworkCurrent>/...`. */
117
+ function linkResolvesIntoFramework(linkPath, fwCurrent) {
118
+ if (!isSymlink(linkPath))
119
+ return false;
120
+ const target = readlinkAbs(linkPath);
121
+ if (!target)
122
+ return false;
123
+ const norm = path_1.default.resolve(target);
124
+ const base = path_1.default.resolve(fwCurrent) + path_1.default.sep;
125
+ return norm === path_1.default.resolve(fwCurrent) || norm.startsWith(base);
126
+ }
127
+ const BAK_SUFFIX = '.pre-symlink.bak';
128
+ /**
129
+ * Convert a workspace's per-workspace framework COPY into symlinks to
130
+ * `framework/current`. See the module docstring for the full safety contract.
131
+ *
132
+ * Runs the whole detect → backup → re-link → verify → cleanup/revert sequence
133
+ * under the registry file-lock so it never races a concurrent framework swap.
134
+ */
135
+ function migrateWorkspaceToSymlinks(slug, projectPath, provider, opts = {}) {
136
+ const fm = opts.framework ?? new framework_manager_1.FrameworkManager({ home: opts.home });
137
+ const fwCurrent = path_1.default.join((0, framework_manager_1.frameworkRoot)(opts.home), 'current');
138
+ // Existence-gate: with no bundled core OR no materialized `current`, no-op.
139
+ if (!fm.isAvailable() || !fs_1.default.existsSync(fwCurrent)) {
140
+ return { outcome: 'no-bundle' };
141
+ }
142
+ return (0, artifact_registry_1.withFileLock)(opts.home, () => runMigration(slug, projectPath, provider, fwCurrent, fm, opts));
143
+ }
144
+ function emit(broadcast, type, extra) {
145
+ try {
146
+ broadcast?.({ type, ...extra });
147
+ }
148
+ catch {
149
+ /* observability is best-effort */
150
+ }
151
+ }
152
+ function runMigration(slug, projectPath, provider, fwCurrent, fm, opts) {
153
+ const providerDir = providerDirFor(provider);
154
+ const ws = (0, workspace_manager_1.workspacePathFor)(slug, opts.home);
155
+ const wsProviderDir = path_1.default.join(ws, providerDir);
156
+ const fwProviderDir = path_1.default.join(fwCurrent, providerDir);
157
+ // Idempotent: if any shared subtree is already a symlink into `current`, the
158
+ // workspace is migrated — no-op.
159
+ for (const sub of SHARED_SUBTREES) {
160
+ if (linkResolvesIntoFramework(path_1.default.join(wsProviderDir, sub), fwCurrent)) {
161
+ return { outcome: 'already-symlinked' };
162
+ }
163
+ }
164
+ // Detect the migratable real-copy layout + the local-divergence guard.
165
+ const cmp = compareSharedSubtrees(wsProviderDir, fwProviderDir);
166
+ if (!cmp.hadSharedSubtree) {
167
+ return { outcome: 'not-a-copy' };
168
+ }
169
+ if (!cmp.matches) {
170
+ emit(opts.broadcast, 'framework.migration_skipped', { slug, provider, reason: 'local-divergence', path: cmp.diverged });
171
+ return { outcome: 'skipped-divergence', detail: cmp.diverged };
172
+ }
173
+ // ── Backup the copied shared subtrees (never delete). ──────────────────────
174
+ const backedUp = [];
175
+ for (const sub of SHARED_SUBTREES) {
176
+ const live = path_1.default.join(wsProviderDir, sub);
177
+ if (!isRealDir(live))
178
+ continue;
179
+ const bak = live + BAK_SUFFIX;
180
+ try {
181
+ // Clear a stale backup from a prior aborted run.
182
+ if (fs_1.default.existsSync(bak))
183
+ fs_1.default.rmSync(bak, { recursive: true, force: true });
184
+ fs_1.default.renameSync(live, bak);
185
+ backedUp.push({ live, bak });
186
+ }
187
+ catch (err) {
188
+ // Could not back up — revert anything already moved and bail (no mutation).
189
+ restoreBackups(backedUp);
190
+ emit(opts.broadcast, 'framework.migration_skipped', { slug, provider, reason: 'backup-failed', path: sub, error: err.message });
191
+ return { outcome: 'skipped-divergence', detail: `backup-failed:${sub}` };
192
+ }
193
+ }
194
+ // ── Re-link via core's offline assemble (re-creates symlinks + agent-memory). ─
195
+ let assembled = false;
196
+ try {
197
+ const res = (0, workspace_manager_1.assembleWorkspaceFramework)(slug, projectPath, provider, {
198
+ home: opts.home,
199
+ framework: fm,
200
+ _skipMigrate: true,
201
+ });
202
+ assembled = res.assembled && !res.error;
203
+ }
204
+ catch {
205
+ assembled = false;
206
+ }
207
+ // ── Preserve agent-memory: if a real agent-memory survived only in a backup,
208
+ // move it into the live layout (assemble seeds an empty one; the user's
209
+ // existing memory must win). ────────────────────────────────────────────────
210
+ preserveAgentMemory(wsProviderDir, backedUp);
211
+ // ── Verify the new symlinks resolve INTO framework/current. ────────────────
212
+ const verified = assembled &&
213
+ SHARED_SUBTREES.some((sub) => linkResolvesIntoFramework(path_1.default.join(wsProviderDir, sub), fwCurrent));
214
+ if (!verified) {
215
+ // REVERT: remove any freshly-created (broken) symlinks/dirs, restore backups.
216
+ for (const { live } of backedUp) {
217
+ try {
218
+ if (fs_1.default.existsSync(live) || isSymlink(live))
219
+ fs_1.default.rmSync(live, { recursive: true, force: true });
220
+ }
221
+ catch {
222
+ /* best-effort */
223
+ }
224
+ }
225
+ restoreBackups(backedUp);
226
+ emit(opts.broadcast, 'framework.migration_skipped', { slug, provider, reason: 'verify-failed' });
227
+ return { outcome: 'reverted', detail: 'verify-failed', backedUp: backedUp.map((b) => b.live) };
228
+ }
229
+ // ── Success cleanup. The shared framework files in each backup were proven
230
+ // byte-identical to `current` by the divergence guard, so deleting them loses
231
+ // nothing. EXCEPTION: a backup may also carry `custom-*` agent files (user /
232
+ // plugin agents that are NOT part of the framework and were ignored by the
233
+ // guard). The live `agents` is now a symlink into the read-only framework, so
234
+ // those custom files have no home there — we must NOT delete them. We retain
235
+ // any backup that holds custom-* content under `<sub>.custom.preserved/` and
236
+ // surface a `framework.custom_agents_preserved` event for observability;
237
+ // backups with only shared framework files are deleted. ──────────────────────
238
+ const preservedCustom = [];
239
+ for (const { live, bak } of backedUp) {
240
+ const customs = listFilesRel(bak).filter(isCustomAgent);
241
+ if (customs.length > 0) {
242
+ const preserved = live + '.custom.preserved';
243
+ try {
244
+ if (fs_1.default.existsSync(preserved))
245
+ fs_1.default.rmSync(preserved, { recursive: true, force: true });
246
+ fs_1.default.renameSync(bak, preserved);
247
+ preservedCustom.push(...customs.map((c) => path_1.default.basename(c)));
248
+ }
249
+ catch {
250
+ /* keep the .bak as-is if we cannot rename — still non-destructive */
251
+ }
252
+ }
253
+ else {
254
+ try {
255
+ fs_1.default.rmSync(bak, { recursive: true, force: true });
256
+ }
257
+ catch {
258
+ /* leaving a stale .bak is harmless; never fail the migration on cleanup */
259
+ }
260
+ }
261
+ }
262
+ if (preservedCustom.length > 0) {
263
+ emit(opts.broadcast, 'framework.custom_agents_preserved', { slug, provider, files: preservedCustom });
264
+ }
265
+ emit(opts.broadcast, 'framework.migrated', { slug, provider, version: fm.bundledVersion() });
266
+ return { outcome: 'migrated', backedUp: backedUp.map((b) => b.live) };
267
+ }
268
+ /** Restore each `<sub>.pre-symlink.bak` back to its live path (revert). */
269
+ function restoreBackups(backedUp) {
270
+ for (const { live, bak } of backedUp) {
271
+ try {
272
+ if (!fs_1.default.existsSync(bak))
273
+ continue;
274
+ if (fs_1.default.existsSync(live) || isSymlink(live))
275
+ fs_1.default.rmSync(live, { recursive: true, force: true });
276
+ fs_1.default.renameSync(bak, live);
277
+ }
278
+ catch {
279
+ /* best-effort revert — a surviving .bak is recoverable by hand */
280
+ }
281
+ }
282
+ }
283
+ /**
284
+ * Ensure the live `agent-memory/` holds the user's existing memory. If the
285
+ * post-assemble layout has an empty/absent agent-memory but a backup carried a
286
+ * populated one, move the backup's agent-memory into the live layout. Never
287
+ * overwrites a non-empty live agent-memory.
288
+ */
289
+ function preserveAgentMemory(wsProviderDir, backedUp) {
290
+ const liveMem = path_1.default.join(wsProviderDir, 'agent-memory');
291
+ // agent-memory was NOT one of the backed-up shared subtrees (it's not in
292
+ // SHARED_SUBTREES), so a copy-layout's agent-memory still sits at the live
293
+ // path untouched. Nothing to move in the common case. This guard only handles
294
+ // a future layout where agent-memory lived under a backed-up subtree.
295
+ for (const { bak } of backedUp) {
296
+ const bakMem = path_1.default.join(bak, 'agent-memory');
297
+ if (!isRealDir(bakMem))
298
+ continue;
299
+ try {
300
+ if (!fs_1.default.existsSync(liveMem)) {
301
+ fs_1.default.renameSync(bakMem, liveMem);
302
+ }
303
+ }
304
+ catch {
305
+ /* best-effort */
306
+ }
307
+ }
308
+ }
@@ -25,6 +25,8 @@ const feature_flags_1 = require("./feature-flags");
25
25
  const mobile_1 = require("./mobile");
26
26
  const telemetry_receiver_1 = require("./telemetry-receiver");
27
27
  const telemetry_compactor_1 = require("./telemetry-compactor");
28
+ const framework_manager_1 = require("./framework-manager");
29
+ const artifact_registry_1 = require("./artifact-registry");
28
30
  const path_resolver_1 = require("./path-resolver");
29
31
  // Side-effect import: registers every bundled ProviderAdapter (claude, codex,
30
32
  // future providers) so `getAdapter`/`hasAdapter`/`listAdapters` are populated
@@ -418,6 +420,34 @@ function applyWsRateLimiting(ws) {
418
420
  registry.loadAll();
419
421
  _registry = registry;
420
422
  _getProjectCount = () => registry.listContexts().length;
423
+ // ─── Bundled framework update channel (Phase 6) ─────────────────────────────
424
+ // After the registry + projects have loaded, run a single framework
425
+ // version-check. On FIRST run it materializes the bundled framework to
426
+ // `~/.specrails/framework/<version>/` + points `current`; POST-UPDATE (a new
427
+ // app version ships a newer bundled core) it materializes the new version
428
+ // side-by-side and atomically swaps `current` → every workspace that links
429
+ // `current/...` jumps to it with no per-workspace work. The whole
430
+ // materialize+swap runs under the SAME registry file-lock that serialises
431
+ // artifact resolution, so a concurrently-spawning rail never observes a
432
+ // half-swapped `current` (a rail that already resolved `current` keeps its
433
+ // resolved framework — the atomic swap doesn't disturb open handles). No-op
434
+ // (graceful) when no bundled core is present — every project falls back to
435
+ // the legacy npx path. The `framework.updated` WS event is app-level (no
436
+ // projectId); the client WS layer ignores unknown message types.
437
+ try {
438
+ const framework = new framework_manager_1.FrameworkManager({
439
+ // `framework.updated` is app-level (no projectId) and not a member of the
440
+ // WsMessage union; cast at this single boundary.
441
+ broadcast: (msg) => broadcast(msg),
442
+ });
443
+ if (framework.isAvailable()) {
444
+ const providers = registry.installedProvidersUnion();
445
+ (0, artifact_registry_1.withFileLock)(undefined, () => framework.versionCheck(providers));
446
+ }
447
+ }
448
+ catch (err) {
449
+ console.error('[framework-manager] startup versionCheck failed (non-fatal):', err);
450
+ }
421
451
  // OTLP/JSON receiver — INTENTIONALLY UNAUTHENTICATED (H-01/H-02). The spawned
422
452
  // claude/codex CLIs post telemetry here with no auth header (queue-manager sets
423
453
  // OTEL_EXPORTER_OTLP_ENDPOINT but no OTEL_EXPORTER_OTLP_HEADERS), so it cannot
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ // install-config-path — the SINGLE resolver for `install-config.yaml`.
3
+ //
4
+ // PRIMARY relocation fix: the install config is core's INPUT at project-creation
5
+ // time (the desktop-written YAML that core reads via `--from-config`). It used to
6
+ // be written into `<project.path>/.specrails/install-config.yaml`, which created
7
+ // a `.specrails/` directory inside the user's repo BEFORE relocation could flip
8
+ // on — the very leak the relocate-artifacts feature exists to prevent.
9
+ //
10
+ // It now lives in the per-project app-managed HOME dir
11
+ // `$HOME/.specrails/projects/<slug>/install-config.yaml`. That directory:
12
+ // - always exists / can always be created (independent of whether the
13
+ // workspace subdir has been populated by core yet), and
14
+ // - is NEVER inside the user's repo, so a fresh setup creates ZERO `.specrails`
15
+ // in the repo.
16
+ //
17
+ // This is deliberately NOT the workspace (`resolveProjectExecution`): the config
18
+ // is read at INSTALL time, before core populates the workspace and before the
19
+ // relocation gate flips. The HOME per-project dir is the one location guaranteed
20
+ // to be available across both the relocated and the non-bundled flows.
21
+ //
22
+ // The slug is allocated by `addProject` (desktop.sqlite) BEFORE any setup runs,
23
+ // so every install-config reader/writer has it. When a slug is somehow absent
24
+ // (defensive — e.g. a synthetic call path), we fall back to a deterministic tmp
25
+ // STAGING path keyed off the repo path — never the repo itself.
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.projectHomeDir = projectHomeDir;
31
+ exports.installConfigPath = installConfigPath;
32
+ exports.ensureInstallConfigDir = ensureInstallConfigDir;
33
+ const fs_1 = __importDefault(require("fs"));
34
+ const os_1 = __importDefault(require("os"));
35
+ const path_1 = __importDefault(require("path"));
36
+ const crypto_1 = require("crypto");
37
+ /** `$HOME` override (mirrors `resolveHome` in artifact-registry / tests). */
38
+ function resolveHome(home) {
39
+ return home ?? process.env.SPECRAILS_REGISTRY_HOME ?? os_1.default.homedir();
40
+ }
41
+ /** True when a slug is a safe single path segment (no separators / traversal). */
42
+ function isSafeSlug(slug) {
43
+ return (typeof slug === 'string' &&
44
+ slug.trim().length > 0 &&
45
+ !slug.includes('/') &&
46
+ !slug.includes('\\') &&
47
+ !slug.includes('..'));
48
+ }
49
+ /** The per-project app-managed HOME dir: `$HOME/.specrails/projects/<slug>`. */
50
+ function projectHomeDir(slug, home) {
51
+ return path_1.default.join(resolveHome(home), '.specrails', 'projects', slug);
52
+ }
53
+ /**
54
+ * Resolve the install-config.yaml path for a project — the per-project HOME dir
55
+ * when the slug is available (the normal case), else a deterministic tmp-staging
56
+ * path keyed off the repo path. NEVER returns a path inside the user's repo.
57
+ */
58
+ function installConfigPath(project, home) {
59
+ if (isSafeSlug(project.slug)) {
60
+ return path_1.default.join(projectHomeDir(project.slug, home), 'install-config.yaml');
61
+ }
62
+ // Defensive fallback: a stable tmp staging dir (NOT the repo) so a slug-less
63
+ // call still never leaks into the user's tree. The hash keeps it per-repo; the
64
+ // file keeps the canonical `install-config.yaml` basename.
65
+ const hash = (0, crypto_1.createHash)('sha256').update(path_1.default.resolve(project.path)).digest('hex').slice(0, 16);
66
+ return path_1.default.join(os_1.default.tmpdir(), `specrails-desktop-install-config-${hash}`, 'install-config.yaml');
67
+ }
68
+ /** Ensure the parent directory of the install-config exists (idempotent). */
69
+ function ensureInstallConfigDir(project, home) {
70
+ const file = installConfigPath(project, home);
71
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
72
+ return file;
73
+ }