specrails-desktop 2.32.0 → 2.34.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 (228) hide show
  1. package/client/dist/assets/{ActivityFeedPage-DvnFkjVG.js → ActivityFeedPage-DIDxNsDj.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-CBjcliD7.js → AgentBrowserCapture-7pJ2G5VY.js} +1 -1
  3. package/client/dist/assets/AgentModeAnalyticsPane-BSSyTnTM.js +2 -0
  4. package/client/dist/assets/{AgentModeCodePane-CokuFVdV.js → AgentModeCodePane-DzW6gvDw.js} +2 -2
  5. package/client/dist/assets/{AgentModeJobsPane-rf_DlWNo.js → AgentModeJobsPane-9wJff15f.js} +1 -1
  6. package/client/dist/assets/AgentsPage-CIA67psb.js +87 -0
  7. package/client/dist/assets/AnalyticsPage-C0LwYELz.js +1 -0
  8. package/client/dist/assets/CodePage-RtKvHmAO.js +2 -0
  9. package/client/dist/assets/{DesktopAnalyticsPage-gBRVqiw6.js → DesktopAnalyticsPage-BU8Y_Ts1.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-BzO5dg2B.js → DocsDialog-DIpoaGlQ.js} +1 -1
  11. package/client/dist/assets/{DocsPage-Db_uxr1j.js → DocsPage-ME47KZpK.js} +1 -1
  12. package/client/dist/assets/InteractiveJobComposer-BGjIe9R6.js +19 -0
  13. package/client/dist/assets/{JobDetailModal-DPHh2WOS.js → JobDetailModal-1UZkqH4I.js} +1 -1
  14. package/client/dist/assets/JobDetailPage-CN65sAf1.js +1 -0
  15. package/client/dist/assets/JobsPage-f-4dqys7.js +1 -0
  16. package/client/dist/assets/LoopBuilderPage-WeiofeL6.js +7 -0
  17. package/client/dist/assets/LoopPreviewModal-DDTnTyH0.js +1 -0
  18. package/client/dist/assets/LoopsPage-BVNchIex.js +1 -0
  19. package/client/dist/assets/MinimizedChatsContext-CHsu3NCX.js +1 -0
  20. package/client/dist/assets/PluginsPage-CdFzGEfQ.js +2 -0
  21. package/client/dist/assets/ProjectSettingsDialog-BkCXBQWk.js +1 -0
  22. package/client/dist/assets/TemplatePreviewModal-RDi0khFG.js +1 -0
  23. package/client/dist/assets/TicketDetailModal-MhJzrqbi.js +14 -0
  24. package/client/dist/assets/{Trans-B2i5HVcM.js → Trans-CTqw6rr9.js} +1 -1
  25. package/client/dist/assets/{addspec-DhmnXER8.js → addspec-A1lDQa4M.js} +1 -1
  26. package/client/dist/assets/{addspec-BRlk8jON.js → addspec-BHZd0N5F.js} +1 -1
  27. package/client/dist/assets/{addspec-CHW9xf_X.js → addspec-BPpZ_olp.js} +1 -1
  28. package/client/dist/assets/{addspec-Di41DpYV.js → addspec-B_TPJt-R.js} +1 -1
  29. package/client/dist/assets/{addspec-4WmLJmBG.js → addspec-CLYLvaVP.js} +1 -1
  30. package/client/dist/assets/{addspec-C7XRejU4.js → addspec-CQBjzt24.js} +1 -1
  31. package/client/dist/assets/{addspec-x3mizDxc.js → addspec-D2YIQ0GU.js} +1 -1
  32. package/client/dist/assets/{addspec-DTmRrWrF.js → addspec-iFdQ6D99.js} +1 -1
  33. package/client/dist/assets/agent-Bd-q3Wqp.js +1 -0
  34. package/client/dist/assets/agent-BlvdW4Hv.js +1 -0
  35. package/client/dist/assets/agent-C-KQhxeM.js +1 -0
  36. package/client/dist/assets/agent-Ca15w5eu.js +1 -0
  37. package/client/dist/assets/agent-D6fKaoMd.js +1 -0
  38. package/client/dist/assets/agent-HFMYMTbB.js +1 -0
  39. package/client/dist/assets/agent-QIEbkBjg.js +1 -0
  40. package/client/dist/assets/agent-mTCsAbCl.js +1 -0
  41. package/client/dist/assets/agentstudio-BNUth0uj.js +1 -0
  42. package/client/dist/assets/agentstudio-BNdB5hkr.js +1 -0
  43. package/client/dist/assets/agentstudio-C7-bt8FO.js +1 -0
  44. package/client/dist/assets/agentstudio-C8uNchpV.js +1 -0
  45. package/client/dist/assets/agentstudio-CKabyMlg.js +1 -0
  46. package/client/dist/assets/agentstudio-CN2pokQu.js +1 -0
  47. package/client/dist/assets/agentstudio-DR7doNBV.js +1 -0
  48. package/client/dist/assets/agentstudio-y2dVDXeM.js +1 -0
  49. package/client/dist/assets/{analytics-C9RaQcE3.js → analytics-47pYygtV.js} +1 -1
  50. package/client/dist/assets/{analytics-B8Qtptbl.js → analytics-B0vt1g5U.js} +1 -1
  51. package/client/dist/assets/analytics-BMzCvrCU.js +1 -0
  52. package/client/dist/assets/{analytics-BPptRY4f.js → analytics-BlFN3Zwx.js} +1 -1
  53. package/client/dist/assets/{analytics-BxDGOIwf.js → analytics-CZYzL-nR.js} +1 -1
  54. package/client/dist/assets/{analytics-D_OZdQ9O.js → analytics-DAsb9MJ8.js} +1 -1
  55. package/client/dist/assets/{analytics-CJIwqrEl.js → analytics-FXQlQ2Yt.js} +1 -1
  56. package/client/dist/assets/{analytics-We6lUMI8.js → analytics-RnFCNkah.js} +1 -1
  57. package/client/dist/assets/{builder-CWYPw7Cw.js → builder-0IWtosUm.js} +1 -1
  58. package/client/dist/assets/{builder-CX1437hF.js → builder-Bf2N6XWn.js} +1 -1
  59. package/client/dist/assets/{builder-BiUG9Z5K.js → builder-BnqaaBkT.js} +1 -1
  60. package/client/dist/assets/{builder-CGRz_-zP.js → builder-BxxfGgAO.js} +1 -1
  61. package/client/dist/assets/{builder-DwDexYRE.js → builder-C5IIgWsq.js} +1 -1
  62. package/client/dist/assets/builder-CHuahItu.js +1 -0
  63. package/client/dist/assets/{builder-Cowk9jtL.js → builder-CjWosXL_.js} +1 -1
  64. package/client/dist/assets/{builder-3bygQtZi.js → builder-CpCIOoKj.js} +1 -1
  65. package/client/dist/assets/dashboard-BHRqsGCV.js +1 -0
  66. package/client/dist/assets/dashboard-Bfr90vOl.js +1 -0
  67. package/client/dist/assets/dashboard-CTsM8x3m.js +1 -0
  68. package/client/dist/assets/dashboard-CqC6wX0h.js +1 -0
  69. package/client/dist/assets/dashboard-CuZtAszc.js +1 -0
  70. package/client/dist/assets/dashboard-DAcqIc-n.js +1 -0
  71. package/client/dist/assets/dashboard-DekiV-86.js +1 -0
  72. package/client/dist/assets/dashboard-DoDczc5S.js +1 -0
  73. package/client/dist/assets/{dist-js-CkTZKBF9.js → dist-js-BmsFWRly.js} +1 -1
  74. package/client/dist/assets/{dist-js-ClShHcGa.js → dist-js-J5W_N5ng.js} +1 -1
  75. package/client/dist/assets/{formatDistanceToNow-BjGwPf6I.js → formatDistanceToNow-BOuXgebD.js} +1 -1
  76. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-CSCbUI4W.js → getTimezoneOffsetInMilliseconds-EjGOpn4b.js} +1 -1
  77. package/client/dist/assets/{i18n-Ch-Db_x2.js → i18n-nJQvD5zJ.js} +2 -2
  78. package/client/dist/assets/index-C_sdsSZt.js +63 -0
  79. package/client/dist/assets/index-DCN5QbDm.css +2 -0
  80. package/client/dist/assets/{jira-BySfUuQZ.js → jira-BNZu8f9O.js} +1 -1
  81. package/client/dist/assets/{jira-gAvOyije.js → jira-BOoaUXbb.js} +1 -1
  82. package/client/dist/assets/{jira-pRaL7JQo.js → jira-BYVN4L2u.js} +1 -1
  83. package/client/dist/assets/jira-CKJg3HZd.js +1 -0
  84. package/client/dist/assets/{jira-Byw2C0F8.js → jira-DDDsZ4ub.js} +1 -1
  85. package/client/dist/assets/{jira-D38Gj7AT.js → jira-DXwYjVE4.js} +1 -1
  86. package/client/dist/assets/{jira-D11gPhxz.js → jira-Dn6U2nRW.js} +1 -1
  87. package/client/dist/assets/{jira-api-BgFIr8eD.js → jira-api-Bhixwn-d.js} +1 -1
  88. package/client/dist/assets/{jira-CNeYi06w.js → jira-j7hVolR5.js} +1 -1
  89. package/client/dist/assets/nav-ApJVg3xu.js +1 -0
  90. package/client/dist/assets/nav-BKNky1Bg.js +1 -0
  91. package/client/dist/assets/nav-C6vWWKYx.js +1 -0
  92. package/client/dist/assets/nav-CXJ6Aw89.js +1 -0
  93. package/client/dist/assets/nav-CsmFBIDf.js +1 -0
  94. package/client/dist/assets/nav-DjkmQJjd.js +1 -0
  95. package/client/dist/assets/nav-iwjCg5FR.js +1 -0
  96. package/client/dist/assets/nav-pYuXq7sf.js +1 -0
  97. package/client/dist/assets/{provider-capabilities-BvoExILy.js → provider-capabilities-Doej9lfa.js} +1 -1
  98. package/client/dist/assets/setup-3jWaoVUO.js +1 -0
  99. package/client/dist/assets/setup-BjgRneb2.js +1 -0
  100. package/client/dist/assets/setup-BzFIjv-f.js +1 -0
  101. package/client/dist/assets/setup-C4VfQsxE.js +1 -0
  102. package/client/dist/assets/setup-DfUkpDbN.js +1 -0
  103. package/client/dist/assets/setup-ECe90Pmu.js +1 -0
  104. package/client/dist/assets/setup-dlqC4Jmm.js +1 -0
  105. package/client/dist/assets/setup-jwfLMiM2.js +1 -0
  106. package/client/dist/assets/{spending-Dq43JYmD.js → spending-BpYPsevQ.js} +1 -1
  107. package/client/dist/assets/tickets-B4rjuSYi.js +1 -0
  108. package/client/dist/assets/tickets-Bc71ce1C.js +1 -0
  109. package/client/dist/assets/tickets-CLg7VTJ8.js +1 -0
  110. package/client/dist/assets/tickets-CP9U1GJt.js +1 -0
  111. package/client/dist/assets/tickets-CZNBiHbq.js +1 -0
  112. package/client/dist/assets/tickets-DlX2mEkD.js +1 -0
  113. package/client/dist/assets/tickets-W6pBmbrX.js +1 -0
  114. package/client/dist/assets/tickets-_yKvCt4Y.js +1 -0
  115. package/client/dist/assets/{useBrowserCaptureSession-TTO8vHtd.js → useBrowserCaptureSession-BBXBN0Na.js} +1 -1
  116. package/client/dist/assets/useDesktop-B6Du2N83.js +1 -0
  117. package/client/dist/assets/{useSharedWebSocket-DaPh7QBL.js → useSharedWebSocket-DK96ctb7.js} +1 -1
  118. package/client/dist/index.html +24 -24
  119. package/docs/guide/de/getting-started/3-adding-your-first-project.md +13 -32
  120. package/docs/guide/de/integrations/1-ai-providers.md +27 -16
  121. package/docs/guide/en/getting-started/3-adding-your-first-project.md +8 -27
  122. package/docs/guide/en/integrations/1-ai-providers.md +21 -18
  123. package/docs/guide/es/getting-started/3-adding-your-first-project.md +12 -31
  124. package/docs/guide/es/integrations/1-ai-providers.md +24 -16
  125. package/docs/guide/fr/getting-started/3-adding-your-first-project.md +15 -34
  126. package/docs/guide/fr/integrations/1-ai-providers.md +25 -16
  127. package/docs/guide/it/getting-started/3-adding-your-first-project.md +16 -35
  128. package/docs/guide/it/integrations/1-ai-providers.md +25 -16
  129. package/docs/guide/ja/getting-started/3-adding-your-first-project.md +16 -35
  130. package/docs/guide/ja/integrations/1-ai-providers.md +7 -12
  131. package/docs/guide/pt/getting-started/3-adding-your-first-project.md +15 -34
  132. package/docs/guide/pt/integrations/1-ai-providers.md +24 -16
  133. package/docs/guide/zh/getting-started/3-adding-your-first-project.md +18 -37
  134. package/docs/guide/zh/integrations/1-ai-providers.md +7 -12
  135. package/docs/internals/project-builder.md +11 -6
  136. package/package.json +1 -1
  137. package/server/dist/ai-invocations.js +35 -0
  138. package/server/dist/blueprint-commit.js +1 -1
  139. package/server/dist/desktop-db.js +22 -1
  140. package/server/dist/desktop-router.js +132 -43
  141. package/server/dist/framework-reseed.js +144 -0
  142. package/server/dist/index.js +41 -2
  143. package/server/dist/jira/jira-sync-manager.js +49 -2
  144. package/server/dist/jira-router.js +21 -0
  145. package/server/dist/job-phase-breakdown.js +132 -0
  146. package/server/dist/legacy-migration.js +395 -0
  147. package/server/dist/loop-executors.js +5 -1
  148. package/server/dist/offline-assemble.js +32 -4
  149. package/server/dist/pr-body.js +13 -0
  150. package/server/dist/project-router-jobs.js +16 -0
  151. package/server/dist/project-router-spending.js +11 -1
  152. package/server/dist/provider-detection.js +222 -0
  153. package/server/dist/provider-selection.js +71 -19
  154. package/server/dist/queue-manager.js +4 -0
  155. package/server/dist/rail-pr-decision.js +10 -0
  156. package/server/dist/rails-router.js +11 -0
  157. package/server/dist/rails-store.js +5 -1
  158. package/server/dist/repo-map.js +293 -0
  159. package/server/dist/setup-manager.js +57 -0
  160. package/server/dist/spending.js +45 -0
  161. package/client/dist/assets/AgentsPage-B648IzGI.js +0 -87
  162. package/client/dist/assets/AnalyticsPage-0rBoULPX.js +0 -1
  163. package/client/dist/assets/CodePage-ju302Vl7.js +0 -2
  164. package/client/dist/assets/InteractiveJobComposer-_jmsE_Az.js +0 -19
  165. package/client/dist/assets/JobDetailPage-7nwHyHGV.js +0 -1
  166. package/client/dist/assets/JobsPage-C_6YrcmE.js +0 -1
  167. package/client/dist/assets/LoopBuilderPage-DJ1UPDwL.js +0 -7
  168. package/client/dist/assets/LoopPreviewModal-12cUnljK.js +0 -1
  169. package/client/dist/assets/LoopsPage-CHYK0Xjr.js +0 -1
  170. package/client/dist/assets/MinimizedChatsContext-Gt6KQySM.js +0 -1
  171. package/client/dist/assets/PluginsPage-E59oQmwc.js +0 -2
  172. package/client/dist/assets/ProjectSettingsDialog-DQTLQ7ie.js +0 -1
  173. package/client/dist/assets/TemplatePreviewModal-nXUcDgfk.js +0 -1
  174. package/client/dist/assets/TicketDetailModal-C-tHW-2s.js +0 -14
  175. package/client/dist/assets/agent-BKsuIgIT.js +0 -1
  176. package/client/dist/assets/agent-Bs9NilfI.js +0 -1
  177. package/client/dist/assets/agent-C23h684q.js +0 -1
  178. package/client/dist/assets/agent-CqEjy3Z2.js +0 -1
  179. package/client/dist/assets/agent-D5DZTks1.js +0 -1
  180. package/client/dist/assets/agent-DE8hUYPl.js +0 -1
  181. package/client/dist/assets/agent-DsTuJGSc.js +0 -1
  182. package/client/dist/assets/agent-DwZM05-x.js +0 -1
  183. package/client/dist/assets/agentstudio-8TQqBenW.js +0 -1
  184. package/client/dist/assets/agentstudio-B8Z_mmEX.js +0 -1
  185. package/client/dist/assets/agentstudio-BZR9YmLs.js +0 -1
  186. package/client/dist/assets/agentstudio-Bge9pAWb.js +0 -1
  187. package/client/dist/assets/agentstudio-Bz169Vzt.js +0 -1
  188. package/client/dist/assets/agentstudio-D6L2jpNi.js +0 -1
  189. package/client/dist/assets/agentstudio-K2-Z7CL0.js +0 -1
  190. package/client/dist/assets/agentstudio-Zmgzthv8.js +0 -1
  191. package/client/dist/assets/analytics-BHqUazhP.js +0 -1
  192. package/client/dist/assets/builder-skfnZ1uC.js +0 -1
  193. package/client/dist/assets/dashboard-BVq6kRJe.js +0 -1
  194. package/client/dist/assets/dashboard-BaKPPmLH.js +0 -1
  195. package/client/dist/assets/dashboard-Bm8e4zYG.js +0 -1
  196. package/client/dist/assets/dashboard-Bt6IesnM.js +0 -1
  197. package/client/dist/assets/dashboard-C_NQZle_.js +0 -1
  198. package/client/dist/assets/dashboard-CyZKoGoI.js +0 -1
  199. package/client/dist/assets/dashboard-HIUdUBQ0.js +0 -1
  200. package/client/dist/assets/dashboard-pm4Mr229.js +0 -1
  201. package/client/dist/assets/index-Bz0TeGRs.css +0 -2
  202. package/client/dist/assets/index-kCUcnT8G.js +0 -77
  203. package/client/dist/assets/jira-CmKWERE0.js +0 -1
  204. package/client/dist/assets/nav-BHDiSIVu.js +0 -1
  205. package/client/dist/assets/nav-CMiYAq4i.js +0 -1
  206. package/client/dist/assets/nav-CqS7NrDW.js +0 -1
  207. package/client/dist/assets/nav-D-0M7xfv.js +0 -1
  208. package/client/dist/assets/nav-D65O6u9I.js +0 -1
  209. package/client/dist/assets/nav-D_LKYvBr.js +0 -1
  210. package/client/dist/assets/nav-FDTJmU79.js +0 -1
  211. package/client/dist/assets/nav-VaB-C4k5.js +0 -1
  212. package/client/dist/assets/setup-BBiHEO7h.js +0 -1
  213. package/client/dist/assets/setup-BlTWhNIQ.js +0 -1
  214. package/client/dist/assets/setup-Bp2L_oiP.js +0 -1
  215. package/client/dist/assets/setup-CInYXp1-.js +0 -1
  216. package/client/dist/assets/setup-CYH3bP6K.js +0 -1
  217. package/client/dist/assets/setup-DwcQ921P.js +0 -1
  218. package/client/dist/assets/setup-PBLjuDSs.js +0 -1
  219. package/client/dist/assets/setup-bx6FtoBp.js +0 -1
  220. package/client/dist/assets/tickets-BL9r1G0e.js +0 -1
  221. package/client/dist/assets/tickets-B_NyHJ8f.js +0 -1
  222. package/client/dist/assets/tickets-BarF1wuG.js +0 -1
  223. package/client/dist/assets/tickets-CclVhpQA.js +0 -1
  224. package/client/dist/assets/tickets-CqqOHbli.js +0 -1
  225. package/client/dist/assets/tickets-DCkFSiqt.js +0 -1
  226. package/client/dist/assets/tickets-Djn3ccfT.js +0 -1
  227. package/client/dist/assets/tickets-MiHBPsy_.js +0 -1
  228. package/client/dist/assets/useDesktop-DUvS4fuP.js +0 -1
@@ -16,6 +16,9 @@ const core_update_manager_1 = require("./core-update-manager");
16
16
  const specrails_tech_client_1 = require("./specrails-tech-client");
17
17
  const core_compat_1 = require("./core-compat");
18
18
  const providers_1 = require("./providers");
19
+ const provider_detection_1 = require("./provider-detection");
20
+ const workspace_manager_1 = require("./workspace-manager");
21
+ const workspace_resolution_1 = require("./workspace-resolution");
19
22
  const desktop_analytics_1 = require("./desktop-analytics");
20
23
  const setup_prerequisites_1 = require("./setup-prerequisites");
21
24
  const path_resolver_1 = require("./path-resolver");
@@ -44,24 +47,9 @@ function allocateSlug(name, existing) {
44
47
  n += 1;
45
48
  return `${base}-${n}`;
46
49
  }
47
- // Emergency rollback for the codex provider: SPECRAILS_CODEX_BETA=0 forces
48
- // codex back to "unavailable" without redeploying. The pre-rebrand
49
- // SPECRAILS_HUB_CODEX_BETA name is read as a legacy fallback when the new
50
- // var is unset (legacy fallback — do not remove while old installs exist).
51
- function isCodexBetaDisabled() {
52
- const v = process.env.SPECRAILS_CODEX_BETA ?? process.env.SPECRAILS_HUB_CODEX_BETA;
53
- return v === '0';
54
- }
55
- // Gemini is enabled by DEFAULT now that its stream-json schema and the full rails
56
- // pipeline (architect→developer→reviewer delegation, headless agent loading, the
57
- // MAX_TURNS resume + reviewer gate) are validated against the live binary
58
- // (0.46/0.47). Emergency rollback: SPECRAILS_GEMINI_BETA=0 forces it back to
59
- // "unavailable" without redeploying — parity with codex's SPECRAILS_CODEX_BETA.
60
- // The adapter is always registered (pricing/getAdapter work); only project
61
- // selection is gated.
62
- function isGeminiBetaDisabled() {
63
- return process.env.SPECRAILS_GEMINI_BETA === '0';
64
- }
50
+ // Beta kill switches live with the app-level detection singleton now
51
+ // (provider-detection.ts) so detection and route gating can never disagree.
52
+ // Re-imported here under the original names.
65
53
  // Theme allow-list. Mirror of THEME_IDS in `client/src/lib/themes.ts` —
66
54
  // kept duplicated to avoid pulling client code into the server bundle.
67
55
  const THEME_ID_ALLOWLIST = new Set(['dracula', 'aurora-light', 'obsidian-dark', 'code-rain', 'specrails', 'galaxy']);
@@ -332,25 +320,64 @@ function createDesktopRouter(registry, broadcast) {
332
320
  db: registry.desktopDb,
333
321
  isLoopRunning: (loopId) => registry.listContexts().some((c) => (0, loop_runs_store_1.countRunningForLoop)(c.db, loopId) > 0),
334
322
  });
335
- // GET /api/projects — list all registered projects
323
+ // Lazy per-provider workspace assembly (provider-auto-detection spec): when
324
+ // the usable set gains a provider, every RELOCATED project whose workspace
325
+ // lacks that provider's surface assembles it in the background — installing
326
+ // codex on Tuesday makes it usable everywhere without user action.
327
+ function assembleNewlyDetected(detected) {
328
+ for (const ctx of registry.listContexts()) {
329
+ try {
330
+ const workspace = (0, workspace_manager_1.workspacePathFor)(ctx.project.slug);
331
+ if (!(0, workspace_resolution_1.isWorkspacePopulated)(workspace))
332
+ continue; // legacy → migration owns it
333
+ const missing = detected.filter((p) => {
334
+ try {
335
+ return (0, providers_1.hasAdapter)(p) && !fs_1.default.existsSync(path_1.default.join(workspace, (0, providers_1.getAdapter)(p).projectDirName));
336
+ }
337
+ catch {
338
+ return false;
339
+ }
340
+ });
341
+ if (missing.length === 0)
342
+ continue;
343
+ ctx.setupManager.startSilentAssemble(ctx.project.id, ctx.project.path, ctx.project.slug, missing);
344
+ }
345
+ catch (err) {
346
+ console.warn(`[desktop] lazy assemble check failed for ${ctx.project.id} (non-fatal):`, err);
347
+ }
348
+ }
349
+ }
350
+ // GET /api/projects — list all registered projects. The old wizard-restore
351
+ // `setupProjectIds` field is retired (silent-project-add): kept as an empty
352
+ // array for wire compat with clients that still destructure it.
336
353
  router.get('/projects', (_req, res) => {
337
- const projects = (0, desktop_db_1.listProjects)(registry.desktopDb);
338
- // Detect projects that are currently in the setup wizard so the client
339
- // can restore the wizard after a page refresh.
340
- const setupProjectIds = [];
341
- for (const p of projects) {
342
- const ctx = registry.getContext(p.id);
343
- if (!ctx)
344
- continue;
345
- const installing = ctx.setupManager.isInstalling(p.id);
346
- const settingUp = ctx.setupManager.isSettingUp(p.id);
347
- const hasSession = !!(0, desktop_db_1.getProjectSetupSession)(registry.desktopDb, p.id);
348
- const specrailsInstalled = hasSpecrails(p.path);
349
- if (installing || settingUp || (hasSession && !specrailsInstalled)) {
350
- setupProjectIds.push(p.id);
354
+ res.json({ projects: (0, desktop_db_1.listProjects)(registry.desktopDb), setupProjectIds: [] });
355
+ });
356
+ // GET /api/providers/detected — the app-level detection snapshot (machine
357
+ // property, not project property). `?refresh=1` bypasses the 60s cache; a
358
+ // changed usable set broadcasts `providers.detected_changed` app-globally.
359
+ router.get('/providers/detected', async (req, res) => {
360
+ try {
361
+ if (req.query.refresh === '1') {
362
+ const { snapshot, changed } = await (0, provider_detection_1.refreshDetection)();
363
+ if (changed) {
364
+ broadcast({
365
+ type: 'providers.detected_changed',
366
+ detected: snapshot.detected,
367
+ providers: snapshot.providers,
368
+ timestamp: new Date().toISOString(),
369
+ });
370
+ assembleNewlyDetected(snapshot.detected);
371
+ }
372
+ res.json(snapshot);
373
+ return;
351
374
  }
375
+ res.json(await (0, provider_detection_1.getDetectionSnapshot)());
376
+ }
377
+ catch (err) {
378
+ console.error('[provider-detection] snapshot error:', err);
379
+ res.status(500).json({ error: 'provider detection failed' });
352
380
  }
353
- res.json({ projects, setupProjectIds });
354
381
  });
355
382
  // GET /api/available-providers — which AI CLIs are installed, plus supported install tiers
356
383
  //
@@ -364,11 +391,11 @@ function createDesktopRouter(registry, broadcast) {
364
391
  // provider surfaces here with no edit. Apply per-provider beta gates: codex
365
392
  // is forced unavailable when SPECRAILS_CODEX_BETA=0 (emergency rollback).
366
393
  const gated = { ...providers };
367
- if (isCodexBetaDisabled())
394
+ if ((0, provider_detection_1.isCodexBetaDisabled)())
368
395
  gated.codex = false;
369
396
  // Gemini: enabled by default; forced unavailable only when
370
397
  // SPECRAILS_GEMINI_BETA=0 (emergency rollback, parity with codex).
371
- if (isGeminiBetaDisabled())
398
+ if ((0, provider_detection_1.isGeminiBetaDisabled)())
372
399
  gated.gemini = false;
373
400
  const providerIssues = {};
374
401
  if (gated.kimi) {
@@ -429,10 +456,28 @@ function createDesktopRouter(registry, broadcast) {
429
456
  res.status(400).json({ error: 'path is required' });
430
457
  return;
431
458
  }
432
- // Normalise to a providers list. New multi-provider clients send
433
- // `providers: ['claude','codex']`; legacy clients send a single
434
- // `provider`; omitting both defaults to ['claude']. The first entry is the
435
- // primary/default provider.
459
+ // Project registration is a detection refresh trigger (spec:
460
+ // provider-auto-detection). The refresh runs in the BACKGROUND so
461
+ // registration latency never pays for CLI probes; the defaulting below uses
462
+ // the last cached snapshot (null before the first startup detection).
463
+ (0, provider_detection_1.refreshDetection)()
464
+ .then(({ snapshot, changed }) => {
465
+ if (!changed)
466
+ return;
467
+ broadcast({
468
+ type: 'providers.detected_changed',
469
+ detected: snapshot.detected,
470
+ providers: snapshot.providers,
471
+ timestamp: new Date().toISOString(),
472
+ });
473
+ assembleNewlyDetected(snapshot.detected);
474
+ })
475
+ .catch(() => { });
476
+ const detectedNow = (0, provider_detection_1.getDetectedIdsSync)() ?? [];
477
+ // Normalise to a providers list. Wire compat: `providers`/`provider` are
478
+ // still accepted and recorded, but the DETECTED set is authoritative for
479
+ // what the project offers (delta spec: multi-provider-architecture).
480
+ // Omitting both registers with the detected set.
436
481
  let providers;
437
482
  if (Array.isArray(providersRaw) && providersRaw.length > 0) {
438
483
  providers = providersRaw;
@@ -441,7 +486,7 @@ function createDesktopRouter(registry, broadcast) {
441
486
  providers = [provider];
442
487
  }
443
488
  else {
444
- providers = ['claude'];
489
+ providers = detectedNow.length > 0 ? [...detectedNow] : ['claude'];
445
490
  }
446
491
  // De-duplicate while preserving order (primary stays first).
447
492
  providers = providers.filter((p, i) => providers.indexOf(p) === i);
@@ -458,13 +503,13 @@ function createDesktopRouter(registry, broadcast) {
458
503
  }
459
504
  // Beta-gate parity: if codex beta is forced off via env, refuse codex
460
505
  // selections too (consistency with /available-providers).
461
- if (providers.includes('codex') && isCodexBetaDisabled()) {
506
+ if (providers.includes('codex') && (0, provider_detection_1.isCodexBetaDisabled)()) {
462
507
  res.status(400).json({
463
508
  error: 'Codex provider is currently disabled (SPECRAILS_CODEX_BETA=0). Unset or set to 1 to enable.',
464
509
  });
465
510
  return;
466
511
  }
467
- if (providers.includes('gemini') && isGeminiBetaDisabled()) {
512
+ if (providers.includes('gemini') && (0, provider_detection_1.isGeminiBetaDisabled)()) {
468
513
  res.status(400).json({
469
514
  error: 'Gemini provider is currently disabled (SPECRAILS_GEMINI_BETA=0). Unset or set to 1 to enable.',
470
515
  });
@@ -527,6 +572,20 @@ function createDesktopRouter(registry, broadcast) {
527
572
  project: ctx.project,
528
573
  timestamp: new Date().toISOString(),
529
574
  });
575
+ // Silent add (global-core-zero-friction): registration is already done —
576
+ // assemble the relocated workspace in the BACKGROUND for every detected
577
+ // provider. Never blocks or rolls back registration. Skipped when the
578
+ // workspace is already populated (re-register of a relocated repo).
579
+ try {
580
+ const workspace = (0, workspace_manager_1.workspacePathFor)(slug);
581
+ const effectiveProviders = detectedNow.length > 0 ? detectedNow : providers;
582
+ if (!(0, workspace_resolution_1.isWorkspacePopulated)(workspace)) {
583
+ ctx.setupManager.startSilentAssemble(id, canonicalPath, slug, effectiveProviders);
584
+ }
585
+ }
586
+ catch (err) {
587
+ console.warn('[desktop] silent assemble launch failed (non-fatal):', err);
588
+ }
530
589
  res.status(201).json({ project: ctx.project, has_specrails: specrailsInstalled });
531
590
  }
532
591
  catch (err) {
@@ -541,6 +600,36 @@ function createDesktopRouter(registry, broadcast) {
541
600
  }
542
601
  }
543
602
  });
603
+ // POST /api/projects/:id/assemble-retry — re-run the silent workspace
604
+ // assemble for the providers that failed (or every detected provider when
605
+ // nothing is recorded as failed). 202; progress rides project.assemble_progress.
606
+ router.post('/projects/:id/assemble-retry', async (req, res) => {
607
+ const ctx = registry.getContext(req.params.id);
608
+ if (!ctx) {
609
+ res.status(404).json({ error: 'unknown project' });
610
+ return;
611
+ }
612
+ const state = ctx.setupManager.silentAssembleState(ctx.project.id);
613
+ if (state.running) {
614
+ res.status(202).json({ alreadyRunning: true });
615
+ return;
616
+ }
617
+ let providers = state.failed;
618
+ if (providers.length === 0) {
619
+ try {
620
+ providers = (await (0, provider_detection_1.getDetectionSnapshot)()).detected;
621
+ }
622
+ catch {
623
+ providers = [];
624
+ }
625
+ }
626
+ if (providers.length === 0) {
627
+ res.status(409).json({ error: 'no providers detected to assemble' });
628
+ return;
629
+ }
630
+ ctx.setupManager.startSilentAssemble(ctx.project.id, ctx.project.path, ctx.project.slug, providers);
631
+ res.status(202).json({ retrying: providers });
632
+ });
544
633
  // DELETE /api/projects/:id — unregister a project
545
634
  router.delete('/projects/:id', (req, res) => {
546
635
  const { id } = req.params;
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ // Post-swap workspace re-seed (global-core-zero-friction / framework-auto-update).
3
+ //
4
+ // A `current` swap updates every SYMLINKED workspace surface for free, but the
5
+ // COPIED files (instruction files with the project name, Windows copy-fallback
6
+ // subtrees, Kimi per-child skill links) stay frozen at the old version. This
7
+ // pass re-runs the offline assemble for every relocated workspace whose
8
+ // recorded framework version differs from `current` — which also repairs
9
+ // machines that were off during a swap (missed-swap scenario).
10
+ //
11
+ // `.mcp.json` guarantee: the framework owns NO mcp keys today, so the surgical
12
+ // "update only framework-owned keys" contract degenerates to full preservation
13
+ // — the file is snapshotted before the assemble and restored byte-identically
14
+ // if the assemble touched it (plugin/user keys can never be clobbered).
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.isFrameworkAutoswapEnabled = isFrameworkAutoswapEnabled;
50
+ exports.readWorkspaceFrameworkVersion = readWorkspaceFrameworkVersion;
51
+ exports.reseedStaleWorkspaces = reseedStaleWorkspaces;
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ const providers_1 = require("./providers");
55
+ const workspace_manager_1 = require("./workspace-manager");
56
+ const workspace_resolution_1 = require("./workspace-resolution");
57
+ const offline_assemble_1 = require("./offline-assemble");
58
+ function isFrameworkAutoswapEnabled() {
59
+ const v = (process.env.SPECRAILS_FRAMEWORK_AUTOSWAP ?? '').toLowerCase();
60
+ return v !== 'false' && v !== '0' && v !== 'off';
61
+ }
62
+ /** The framework version this workspace was last assembled against. */
63
+ function readWorkspaceFrameworkVersion(slug) {
64
+ try {
65
+ const v = fs.readFileSync(path.join((0, workspace_manager_1.workspacePathFor)(slug), '.specrails', 'specrails-version'), 'utf-8').trim();
66
+ return v.length > 0 ? v : null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** Providers whose surface exists in this workspace (assembled at some point). */
73
+ function workspaceProviders(workspace) {
74
+ return (0, providers_1.listAdapters)()
75
+ .filter((a) => fs.existsSync(path.join(workspace, a.projectDirName)))
76
+ .map((a) => a.id);
77
+ }
78
+ /**
79
+ * Re-seed every relocated workspace whose recorded framework version differs
80
+ * from `currentVersion`. Idempotent (an up-to-date workspace is skipped);
81
+ * serialized; never throws — per-project failures are reported and logged.
82
+ */
83
+ async function reseedStaleWorkspaces(projects, currentVersion, io) {
84
+ const results = [];
85
+ if (!currentVersion)
86
+ return results;
87
+ const assemble = io?.assemble
88
+ ?? (async (p, providers) => {
89
+ await (0, offline_assemble_1.assembleProjectOffline)({
90
+ projectPath: p.path,
91
+ slug: p.slug,
92
+ desktopProjectId: p.id,
93
+ providers,
94
+ continueOnError: true,
95
+ });
96
+ });
97
+ for (const project of projects) {
98
+ const workspace = (0, workspace_manager_1.workspacePathFor)(project.slug);
99
+ if (!(0, workspace_resolution_1.isWorkspacePopulated)(workspace)) {
100
+ results.push({ projectId: project.id, reseeded: false, skippedReason: 'not-relocated' });
101
+ continue;
102
+ }
103
+ const recorded = readWorkspaceFrameworkVersion(project.slug);
104
+ if (recorded === currentVersion) {
105
+ results.push({ projectId: project.id, reseeded: false, skippedReason: 'up-to-date' });
106
+ continue;
107
+ }
108
+ const providers = workspaceProviders(workspace);
109
+ if (providers.length === 0) {
110
+ results.push({ projectId: project.id, reseeded: false, skippedReason: 'not-relocated' });
111
+ continue;
112
+ }
113
+ // `.mcp.json` preservation snapshot (see module header).
114
+ const mcpPath = path.join(workspace, '.mcp.json');
115
+ let mcpBefore = null;
116
+ try {
117
+ mcpBefore = fs.readFileSync(mcpPath, 'utf-8');
118
+ }
119
+ catch { /* absent */ }
120
+ try {
121
+ await assemble(project, providers);
122
+ if (mcpBefore !== null) {
123
+ let mcpAfter = null;
124
+ try {
125
+ mcpAfter = fs.readFileSync(mcpPath, 'utf-8');
126
+ }
127
+ catch { /* deleted by assemble */ }
128
+ if (mcpAfter !== mcpBefore)
129
+ fs.writeFileSync(mcpPath, mcpBefore, 'utf-8');
130
+ }
131
+ results.push({ projectId: project.id, reseeded: true });
132
+ console.log(`[framework-reseed] ${project.slug}: ${recorded ?? '(none)'} → ${currentVersion}`);
133
+ }
134
+ catch (err) {
135
+ results.push({
136
+ projectId: project.id,
137
+ reseeded: false,
138
+ error: err instanceof Error ? err.message : String(err),
139
+ });
140
+ console.warn(`[framework-reseed] ${project.slug} failed (non-fatal):`, err);
141
+ }
142
+ }
143
+ return results;
144
+ }
@@ -15,6 +15,11 @@ const express_1 = __importDefault(require("express"));
15
15
  const ws_1 = require("ws");
16
16
  const project_registry_1 = require("./project-registry");
17
17
  const desktop_router_1 = require("./desktop-router");
18
+ const provider_detection_1 = require("./provider-detection");
19
+ const provider_selection_1 = require("./provider-selection");
20
+ const desktop_db_1 = require("./desktop-db");
21
+ const legacy_migration_1 = require("./legacy-migration");
22
+ const framework_reseed_1 = require("./framework-reseed");
18
23
  const project_router_1 = require("./project-router");
19
24
  const docs_router_1 = require("./docs-router");
20
25
  const auth_1 = require("./auth");
@@ -522,9 +527,20 @@ function applyPtyWsRateLimiting(ws) {
522
527
  // WsMessage union; cast at this single boundary.
523
528
  broadcast: (msg) => broadcast(msg),
524
529
  });
525
- if (framework.isAvailable()) {
530
+ // Kill switch (framework-auto-update spec): SPECRAILS_FRAMEWORK_AUTOSWAP=false
531
+ // restores the manual-only flow (GlobalSettings "check now").
532
+ if (framework.isAvailable() && (0, framework_reseed_1.isFrameworkAutoswapEnabled)()) {
526
533
  const providers = registry.installedProvidersUnion();
527
- (0, artifact_registry_1.withFileLock)(undefined, () => framework.versionCheck(providers));
534
+ const check = (0, artifact_registry_1.withFileLock)(undefined, () => framework.versionCheck(providers));
535
+ // Post-swap re-seed: refresh COPIED workspace files (instruction files,
536
+ // Windows copy-fallback, Kimi skill links) for every workspace whose
537
+ // recorded framework version ≠ current — also repairs missed swaps.
538
+ const reseedProjects = registry.listContexts().map((c) => ({
539
+ id: c.project.id,
540
+ slug: c.project.slug,
541
+ path: c.project.path,
542
+ }));
543
+ void (0, framework_reseed_1.reseedStaleWorkspaces)(reseedProjects, check.version);
528
544
  }
529
545
  }
530
546
  catch (err) {
@@ -542,6 +558,29 @@ function applyPtyWsRateLimiting(ws) {
542
558
  (0, telemetry_compactor_1.runCompactionForAll)(registry).catch((err) => {
543
559
  console.error('[telemetry-compactor] startup compaction error:', err);
544
560
  });
561
+ // ─── App-level provider detection (global-core-zero-friction) ───────────────
562
+ // Providers are a machine property: one detection snapshot feeds
563
+ // provider-selection (per-invocation validation) and desktop-db's project-row
564
+ // mirror (`providers` reads as the detected set). Startup is a refresh
565
+ // trigger; window-focus and project-add refreshes ride the REST route.
566
+ (0, provider_selection_1.setDetectedProvidersSupplier)(provider_detection_1.getDetectedIdsSync);
567
+ (0, desktop_db_1.setProjectProvidersMirror)(provider_detection_1.getDetectedIdsSync);
568
+ (0, provider_detection_1.refreshDetection)()
569
+ .then(({ snapshot }) => {
570
+ // ─── Forced legacy migration (global-core-zero-friction) ────────────────
571
+ // After the first detection lands, migrate repo-resident core installs to
572
+ // the relocated workspace in the background — serialized, fail-open,
573
+ // journaled. Kill switch: SPECRAILS_LEGACY_MIGRATION=false.
574
+ const projects = registry.listContexts().map((c) => ({
575
+ id: c.project.id,
576
+ slug: c.project.slug,
577
+ path: c.project.path,
578
+ }));
579
+ void (0, legacy_migration_1.runLegacyMigrationSweep)(projects, { providers: snapshot.detected });
580
+ })
581
+ .catch((err) => {
582
+ console.error('[provider-detection] startup detection failed (non-fatal):', err);
583
+ });
545
584
  // ─── Mobile companion gateway (off by default; boot if previously enabled) ──
546
585
  // Second HTTPS+WSS listener in THIS process; the main server stays loopback.
547
586
  // The control plane is desktop-only: loopback + (via /api below) requireAuth.
@@ -687,6 +687,49 @@ class JiraSyncManager {
687
687
  * reconciles it to the issue's real status (protected meanwhile by the
688
688
  * pending-transition frozen guard).
689
689
  */
690
+ /**
691
+ * Manual "move to <Jira status>" from the spec detail's status selector
692
+ * (global-core-zero-friction follow-up): enqueue an explicit-target
693
+ * transition to the NAMED raw Jira status. The logical state is derived by
694
+ * reverse statusMap lookup (falling back to `in_progress` — the resolver
695
+ * honours the explicit target regardless), and the local cache is
696
+ * optimistically flipped so the board reflects the move immediately; the
697
+ * inbound poll reconciles to the issue's real status afterwards.
698
+ */
699
+ moveSpecToStatus(localId, targetStatus) {
700
+ if (!this.isActive())
701
+ return { ok: false, reason: 'not-active' };
702
+ const link = (0, jira_db_1.getLinkByLocalId)(this.db, localId);
703
+ if (!link || link.tombstoned)
704
+ return { ok: false, reason: 'no-link' };
705
+ const conn = (0, jira_db_1.getConnection)(this.db, this.projectId);
706
+ const map = conn?.statusMap ?? null;
707
+ let logical = 'in_progress';
708
+ if (map) {
709
+ for (const [state, name] of Object.entries(map)) {
710
+ if (typeof name === 'string' && name.toLowerCase() === targetStatus.toLowerCase()) {
711
+ logical = state;
712
+ break;
713
+ }
714
+ }
715
+ }
716
+ const nonce = Date.now().toString(36);
717
+ (0, jira_db_1.enqueueMany)(this.db, [{
718
+ jiraIssueId: link.jiraIssueId,
719
+ opType: 'transition',
720
+ idempotencyKey: `manual-move:${localId}:${nonce}`,
721
+ payload: { localId, jiraIssueId: link.jiraIssueId, logicalState: logical, targetStatus },
722
+ }]);
723
+ const localStatus = logical === 'todo' ? 'todo'
724
+ : logical === 'done' ? 'done'
725
+ : logical === 'cancelled' ? 'cancelled'
726
+ : logical === 'on_review' ? 'on_review'
727
+ : 'in_progress';
728
+ this.writeLocalStatus([localId], localStatus, targetStatus);
729
+ this.broadcastOutboxState();
730
+ void this.drainOnce().catch(() => undefined);
731
+ return { ok: true };
732
+ }
690
733
  discardSpec(localId, comment) {
691
734
  if (!this.isActive())
692
735
  return { ok: false, reason: 'not-active' };
@@ -1156,7 +1199,7 @@ class JiraSyncManager {
1156
1199
  void this.drainOnce().catch(() => undefined);
1157
1200
  }
1158
1201
  // ─── Local cache helpers ───────────────────────────────────────────────────
1159
- writeLocalStatus(localIds, status) {
1202
+ writeLocalStatus(localIds, status, rawJiraStatus) {
1160
1203
  if (localIds.length === 0)
1161
1204
  return;
1162
1205
  try {
@@ -1166,8 +1209,12 @@ class JiraSyncManager {
1166
1209
  const store = (0, ticket_store_1.mutateStore)(file, (s) => {
1167
1210
  for (const id of ids) {
1168
1211
  const t = s.tickets[id];
1169
- if (t && t.status !== status) {
1212
+ if (t && (t.status !== status || (rawJiraStatus !== undefined && t.jira_status !== rawJiraStatus))) {
1170
1213
  t.status = status;
1214
+ // Manual move-to-status: reflect the RAW target immediately so the
1215
+ // Jira-status filter dimension updates without waiting for the poll.
1216
+ if (rawJiraStatus !== undefined)
1217
+ t.jira_status = rawJiraStatus;
1171
1218
  t.updated_at = now;
1172
1219
  }
1173
1220
  }
@@ -245,6 +245,27 @@ function createJiraRouter() {
245
245
  }
246
246
  res.status(201).json({ localId: result.localId, jiraKey: result.jiraKey });
247
247
  });
248
+ // POST /specs/:localId/move-to-status — manual transition to a NAMED raw Jira
249
+ // status from the spec detail's status selector. Body: { status: string }.
250
+ router.post('/specs/:localId/move-to-status', (req, res) => {
251
+ const c = ctx(req);
252
+ const localId = parseInt(req.params.localId, 10);
253
+ if (Number.isNaN(localId)) {
254
+ res.status(400).json({ error: 'Invalid spec id' });
255
+ return;
256
+ }
257
+ const status = req.body?.status;
258
+ if (!isNonEmptyString(status)) {
259
+ res.status(400).json({ error: 'status is required' });
260
+ return;
261
+ }
262
+ const result = c.jiraSyncManager.moveSpecToStatus(localId, status.trim());
263
+ if (!result.ok) {
264
+ res.status(result.reason === 'no-link' ? 404 : 409).json({ error: result.reason });
265
+ return;
266
+ }
267
+ res.status(202).json({ ok: true });
268
+ });
248
269
  // POST /specs/:localId/discard — "Move to <discard status>": transition the
249
270
  // linked issue to the configured discard status (+ optional reason comment)
250
271
  // instead of a destructive delete. Body: { comment?: string }.