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
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeJobPhaseBreakdown = computeJobPhaseBreakdown;
4
+ const pricing_1 = require("./pricing");
5
+ function computeJobPhaseBreakdown(events, providerId = 'claude') {
6
+ const phases = new Map();
7
+ /** tool_use id → root phase id, for transitive nested-Task attribution. */
8
+ const rootOf = new Map();
9
+ const orchestrator = emptyTotals();
10
+ const unattributed = emptyTotals();
11
+ let sawUsage = false;
12
+ const totalsFor = (parentId) => {
13
+ if (!parentId)
14
+ return orchestrator;
15
+ const rootId = rootOf.get(parentId);
16
+ if (!rootId)
17
+ return unattributed;
18
+ return phases.get(rootId) ?? unattributed;
19
+ };
20
+ for (const ev of events) {
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(ev.payload);
24
+ }
25
+ catch {
26
+ continue;
27
+ }
28
+ const parentId = typeof parsed.parent_tool_use_id === 'string' ? parsed.parent_tool_use_id : null;
29
+ const message = parsed.message;
30
+ if (parsed.type === 'assistant' && message) {
31
+ // Register Task spawns FIRST so a same-event usage envelope can't
32
+ // attribute to a phase this very event opens.
33
+ for (const block of contentBlocks(message)) {
34
+ if (block.type !== 'tool_use' || block.name !== 'Task' || typeof block.id !== 'string')
35
+ continue;
36
+ const input = (block.input ?? {});
37
+ const rootId = parentId ? (rootOf.get(parentId) ?? null) : null;
38
+ if (rootId) {
39
+ // Nested Task: its events roll up into the ancestor phase.
40
+ rootOf.set(block.id, rootId);
41
+ }
42
+ else {
43
+ rootOf.set(block.id, block.id);
44
+ phases.set(block.id, {
45
+ ...emptyTotals(),
46
+ agent: typeof input.subagent_type === 'string' ? input.subagent_type : 'subagent',
47
+ toolUseId: block.id,
48
+ description: typeof input.description === 'string' ? input.description : null,
49
+ startedAt: ev.timestamp,
50
+ endedAt: null,
51
+ durationMs: null,
52
+ });
53
+ }
54
+ }
55
+ const usage = message.usage;
56
+ if (usage && typeof usage === 'object') {
57
+ sawUsage = true;
58
+ const target = totalsFor(parentId);
59
+ const tokensIn = num(usage.input_tokens);
60
+ const tokensOut = num(usage.output_tokens);
61
+ const cacheRead = num(usage.cache_read_input_tokens);
62
+ const cacheCreate = num(usage.cache_creation_input_tokens);
63
+ target.tokensIn += tokensIn;
64
+ target.tokensOut += tokensOut;
65
+ target.tokensCacheRead += cacheRead;
66
+ target.tokensCacheCreate += cacheCreate;
67
+ target.assistantEvents += 1;
68
+ const cost = (0, pricing_1.estimateCostUsd)(providerId, str(message.model), {
69
+ tokens_in: tokensIn,
70
+ tokens_out: tokensOut,
71
+ tokens_cache_read: cacheRead,
72
+ tokens_cache_create: cacheCreate,
73
+ });
74
+ if (cost !== null) {
75
+ target.estimatedCostUsd = (target.estimatedCostUsd ?? 0) + cost;
76
+ }
77
+ }
78
+ continue;
79
+ }
80
+ if (parsed.type === 'user' && message) {
81
+ // A tool_result closing a root Task block ends that phase.
82
+ for (const block of contentBlocks(message)) {
83
+ if (block.type !== 'tool_result' || typeof block.tool_use_id !== 'string')
84
+ continue;
85
+ const phase = phases.get(block.tool_use_id);
86
+ if (phase && phase.endedAt === null) {
87
+ phase.endedAt = ev.timestamp;
88
+ phase.durationMs = spanMs(phase.startedAt, ev.timestamp);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ if (!sawUsage)
94
+ return null;
95
+ const hasUnattributed = unattributed.assistantEvents > 0 || unattributed.tokensIn > 0 || unattributed.tokensOut > 0;
96
+ return {
97
+ phases: [...phases.values()],
98
+ orchestrator,
99
+ unattributed: hasUnattributed ? unattributed : null,
100
+ estimated: true,
101
+ };
102
+ }
103
+ // ─── Internals ────────────────────────────────────────────────────────────────
104
+ function emptyTotals() {
105
+ return {
106
+ tokensIn: 0,
107
+ tokensOut: 0,
108
+ tokensCacheRead: 0,
109
+ tokensCacheCreate: 0,
110
+ assistantEvents: 0,
111
+ estimatedCostUsd: null,
112
+ };
113
+ }
114
+ function contentBlocks(message) {
115
+ const content = message.content;
116
+ return Array.isArray(content) ? content : [];
117
+ }
118
+ function num(value) {
119
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
120
+ }
121
+ function str(value) {
122
+ return typeof value === 'string' ? value : null;
123
+ }
124
+ function spanMs(start, end) {
125
+ if (!start || !end)
126
+ return null;
127
+ const a = Date.parse(start);
128
+ const b = Date.parse(end);
129
+ if (Number.isNaN(a) || Number.isNaN(b) || b < a)
130
+ return null;
131
+ return b - a;
132
+ }
@@ -0,0 +1,395 @@
1
+ "use strict";
2
+ // Forced migration of legacy repo-resident specrails-core installs
3
+ // (global-core-zero-friction / legacy-install-migration spec).
4
+ //
5
+ // A "legacy" project has core installed INSIDE the user's repo (the pre-
6
+ // relocation layout): `<repo>/.specrails/specrails-version` exists and the
7
+ // relocated workspace is not populated. Migration, per project and serialized:
8
+ // 1. assemble the relocated workspace from the bundled framework,
9
+ // 2. MOVE per-project state from `<repo>/.specrails/` (and provider
10
+ // `agent-memory/`) into the workspace,
11
+ // 3. CLEAN the repo with a manifest — exact framework listing (from
12
+ // `~/.specrails/framework/current`) UNION narrow historical patterns
13
+ // (`sr-*` agents, `commands/{sr,specrails,opsx}`, `sr-*`/`specrails-*`
14
+ // skills) that older core versions installed,
15
+ // 4. journal every action write-ahead to
16
+ // `~/.specrails/projects/<slug>/migration-log.json` (fail-open, resumable).
17
+ //
18
+ // NEVER touched: `openspec/**`, `<providerDir>/worktrees/**`, `custom-*` files,
19
+ // user instruction files (CLAUDE.md/AGENTS.md/GEMINI.md), user settings, and
20
+ // `.mcp.json` keys we do not own (surgical key removal only).
21
+ //
22
+ // Kill switch: SPECRAILS_LEGACY_MIGRATION=false disables the whole sweep.
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.isLegacyMigrationEnabled = isLegacyMigrationEnabled;
58
+ exports.isLegacyProject = isLegacyProject;
59
+ exports.journalPathFor = journalPathFor;
60
+ exports.buildCleanupManifest = buildCleanupManifest;
61
+ exports.isProtectedPath = isProtectedPath;
62
+ exports.readJournal = readJournal;
63
+ exports.migrateProject = migrateProject;
64
+ exports.runLegacyMigrationSweep = runLegacyMigrationSweep;
65
+ const fs = __importStar(require("fs"));
66
+ const path = __importStar(require("path"));
67
+ const providers_1 = require("./providers");
68
+ const artifact_registry_1 = require("./artifact-registry");
69
+ const workspace_manager_1 = require("./workspace-manager");
70
+ const workspace_resolution_1 = require("./workspace-resolution");
71
+ const offline_assemble_1 = require("./offline-assemble");
72
+ const plugin_manager_1 = require("./plugin-manager");
73
+ const plugins_1 = require("./plugins");
74
+ function isLegacyMigrationEnabled() {
75
+ const v = (process.env.SPECRAILS_LEGACY_MIGRATION ?? '').toLowerCase();
76
+ return v !== 'false' && v !== '0' && v !== 'off';
77
+ }
78
+ /** True when the project still runs from a repo-resident core install. */
79
+ function isLegacyProject(project) {
80
+ if (!fs.existsSync(path.join(project.path, '.specrails', 'specrails-version')))
81
+ return false;
82
+ return !(0, workspace_resolution_1.isWorkspacePopulated)((0, workspace_manager_1.workspacePathFor)(project.slug));
83
+ }
84
+ function journalPathFor(slug) {
85
+ return path.join((0, artifact_registry_1.resolveHome)(), 'projects', slug, 'migration-log.json');
86
+ }
87
+ // ─── Manifest ────────────────────────────────────────────────────────────────
88
+ /** Legacy command namespaces older cores installed (deprecated `sr` included). */
89
+ const HISTORICAL_COMMAND_DIRS = ['sr', 'specrails', 'opsx'];
90
+ function isFrameworkOwnedName(name) {
91
+ // Framework naming conventions ONLY — `custom-*`, `openspec-*` and any other
92
+ // user/third-party name can never match.
93
+ return name.startsWith('sr-') || name.startsWith('specrails-');
94
+ }
95
+ function listFrameworkRelPaths(providerDir, home) {
96
+ // Exact relative paths (under the provider dir) from the CURRENT framework
97
+ // version — covers files the historical patterns don't (e.g. rules).
98
+ const root = path.join(home, 'framework', 'current', providerDir);
99
+ const out = [];
100
+ const walk = (dir, rel) => {
101
+ let entries;
102
+ try {
103
+ entries = fs.readdirSync(dir, { withFileTypes: true });
104
+ }
105
+ catch {
106
+ return;
107
+ }
108
+ for (const e of entries) {
109
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
110
+ if (e.isDirectory())
111
+ walk(path.join(dir, e.name), childRel);
112
+ else
113
+ out.push(childRel);
114
+ }
115
+ };
116
+ walk(root, '');
117
+ return out;
118
+ }
119
+ /**
120
+ * The repo paths (absolute) migration is allowed to delete. Exact framework
121
+ * listing ∪ narrow historical patterns. Only existing paths are returned.
122
+ */
123
+ function buildCleanupManifest(repoPath, home = (0, artifact_registry_1.resolveHome)()) {
124
+ const out = [];
125
+ const seen = new Set();
126
+ const add = (abs, reason) => {
127
+ if (seen.has(abs))
128
+ return;
129
+ if (!fs.existsSync(abs))
130
+ return;
131
+ if (isProtectedPath(repoPath, abs))
132
+ return;
133
+ seen.add(abs);
134
+ out.push({ path: abs, reason });
135
+ };
136
+ for (const adapter of (0, providers_1.listAdapters)()) {
137
+ const pd = path.join(repoPath, adapter.projectDirName);
138
+ if (!fs.existsSync(pd))
139
+ continue;
140
+ // Historical pattern: framework agents.
141
+ const agentsDir = path.join(pd, 'agents');
142
+ try {
143
+ for (const f of fs.readdirSync(agentsDir)) {
144
+ if (isFrameworkOwnedName(f) && f.endsWith('.md')) {
145
+ add(path.join(agentsDir, f), 'framework agent (sr-*)');
146
+ }
147
+ }
148
+ }
149
+ catch { /* no agents dir */ }
150
+ // Historical pattern: framework command namespaces (whole dirs).
151
+ for (const ns of HISTORICAL_COMMAND_DIRS) {
152
+ add(path.join(pd, 'commands', ns), `framework command namespace (${ns})`);
153
+ }
154
+ // Historical pattern: framework skills (per-child, never openspec-*/custom-*).
155
+ const skillsDir = path.join(pd, 'skills');
156
+ try {
157
+ for (const child of fs.readdirSync(skillsDir)) {
158
+ if (isFrameworkOwnedName(child)) {
159
+ add(path.join(skillsDir, child), 'framework skill (sr-*/specrails-*)');
160
+ }
161
+ }
162
+ }
163
+ catch { /* no skills dir */ }
164
+ // Exact current-framework listing (covers rules and anything pattern-less).
165
+ for (const rel of listFrameworkRelPaths(adapter.projectDirName, home)) {
166
+ add(path.join(pd, rel), 'exact framework listing match');
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ /** Hard exclusions — checked even though the manifest never generates them. */
172
+ function isProtectedPath(repoPath, abs) {
173
+ const rel = path.relative(repoPath, abs);
174
+ if (rel.startsWith('..'))
175
+ return true; // outside the repo — never
176
+ const parts = rel.split(path.sep);
177
+ if (parts[0] === 'openspec')
178
+ return true;
179
+ if (parts.includes('worktrees'))
180
+ return true;
181
+ const base = path.basename(abs);
182
+ if (base.startsWith('custom-'))
183
+ return true;
184
+ if (base.startsWith('openspec'))
185
+ return true;
186
+ if (['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'].includes(base))
187
+ return true;
188
+ if (base === 'settings.json' || base === 'settings.local.json')
189
+ return true;
190
+ if (base === '.mcp.json')
191
+ return true; // handled surgically, never deleted
192
+ return false;
193
+ }
194
+ // ─── State move ──────────────────────────────────────────────────────────────
195
+ const STATE_ENTRIES = [
196
+ 'profiles',
197
+ 'local-tickets.json',
198
+ 'backlog-config.json',
199
+ 'state',
200
+ 'file-summaries',
201
+ 'plugins',
202
+ ];
203
+ function planStateMoves(repoPath, workspace) {
204
+ const moves = [];
205
+ const repoSpecrails = path.join(repoPath, '.specrails');
206
+ for (const entry of STATE_ENTRIES) {
207
+ const src = path.join(repoSpecrails, entry);
208
+ if (fs.existsSync(src)) {
209
+ moves.push({ src, dest: path.join(workspace, '.specrails', entry), reason: `per-project state (${entry})` });
210
+ }
211
+ }
212
+ // Provider agent-memory (real per-project state, never framework).
213
+ for (const adapter of (0, providers_1.listAdapters)()) {
214
+ const src = path.join(repoPath, adapter.projectDirName, 'agent-memory');
215
+ if (fs.existsSync(src)) {
216
+ moves.push({ src, dest: path.join(workspace, adapter.projectDirName, 'agent-memory'), reason: 'agent memory' });
217
+ }
218
+ }
219
+ return moves;
220
+ }
221
+ function moveOverwriting(src, dest) {
222
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
223
+ // Repo state is the REAL user data — it wins over freshly-seeded workspace
224
+ // defaults (e.g. the seeded default profile).
225
+ fs.rmSync(dest, { recursive: true, force: true });
226
+ try {
227
+ fs.renameSync(src, dest);
228
+ }
229
+ catch {
230
+ // Cross-device fallback.
231
+ fs.cpSync(src, dest, { recursive: true });
232
+ fs.rmSync(src, { recursive: true, force: true });
233
+ }
234
+ }
235
+ // ─── Journal ─────────────────────────────────────────────────────────────────
236
+ function writeJournal(journalPath, journal) {
237
+ fs.mkdirSync(path.dirname(journalPath), { recursive: true });
238
+ const tmp = `${journalPath}.tmp`;
239
+ fs.writeFileSync(tmp, JSON.stringify(journal, null, 2), 'utf-8');
240
+ fs.renameSync(tmp, journalPath);
241
+ }
242
+ function readJournal(journalPath) {
243
+ try {
244
+ const parsed = JSON.parse(fs.readFileSync(journalPath, 'utf-8'));
245
+ return parsed && parsed.schemaVersion === 1 && Array.isArray(parsed.entries) ? parsed : null;
246
+ }
247
+ catch {
248
+ return null;
249
+ }
250
+ }
251
+ // ─── Per-project migration ───────────────────────────────────────────────────
252
+ async function migrateProject(project, io) {
253
+ const journalPath = journalPathFor(project.slug);
254
+ // Resume path: a prior crash left unexecuted entries — re-attempt those only.
255
+ const prior = readJournal(journalPath);
256
+ const resuming = prior !== null && !prior.finishedAt && prior.entries.some((e) => !e.executed);
257
+ if (!resuming && !isLegacyProject(project)) {
258
+ return { projectId: project.id, migrated: false, skippedReason: 'not-legacy' };
259
+ }
260
+ const workspace = (0, workspace_manager_1.workspacePathFor)(project.slug);
261
+ // 1. Ensure the relocated workspace exists (idempotent, continue-on-error —
262
+ // the state move below is what flips the activation gate).
263
+ if (!(0, workspace_resolution_1.isWorkspacePopulated)(workspace)) {
264
+ const providers = io?.providers ?? (0, providers_1.listAdapters)().map((a) => a.id);
265
+ const assemble = io?.assemble
266
+ ?? (async (p, provs) => {
267
+ await (0, offline_assemble_1.assembleProjectOffline)({
268
+ projectPath: p.path,
269
+ slug: p.slug,
270
+ desktopProjectId: p.id,
271
+ providers: provs,
272
+ continueOnError: true,
273
+ });
274
+ });
275
+ try {
276
+ await assemble(project, providers);
277
+ }
278
+ catch (err) {
279
+ return {
280
+ projectId: project.id,
281
+ migrated: false,
282
+ error: `workspace assemble failed: ${err instanceof Error ? err.message : String(err)}`,
283
+ };
284
+ }
285
+ }
286
+ // 2+3. Plan (write-ahead) then execute.
287
+ const journal = resuming
288
+ ? prior
289
+ : {
290
+ schemaVersion: 1,
291
+ projectId: project.id,
292
+ repoPath: project.path,
293
+ startedAt: new Date().toISOString(),
294
+ entries: [],
295
+ };
296
+ if (!resuming) {
297
+ for (const m of planStateMoves(project.path, workspace)) {
298
+ journal.entries.push({ action: 'move', src: m.src, dest: m.dest, reason: m.reason, executed: false });
299
+ }
300
+ for (const d of buildCleanupManifest(project.path)) {
301
+ journal.entries.push({ action: 'delete', src: d.path, reason: d.reason, executed: false });
302
+ }
303
+ // The now-empty app-owned `.specrails/` residue (version marker etc.) —
304
+ // fully ours by the reserved-paths contract.
305
+ journal.entries.push({
306
+ action: 'delete',
307
+ src: path.join(project.path, '.specrails'),
308
+ reason: 'app-owned .specrails residue',
309
+ executed: false,
310
+ });
311
+ // Surgical .mcp.json cleanup: only bundled-plugin-owned keys.
312
+ const ownedKeys = plugins_1.BUNDLED_PLUGINS.flatMap((p) => p.manifest.owns.mcpServers ?? []);
313
+ if (ownedKeys.length > 0 && fs.existsSync(path.join(project.path, '.mcp.json'))) {
314
+ journal.entries.push({
315
+ action: 'mcp-key-remove',
316
+ src: path.join(project.path, '.mcp.json'),
317
+ reason: `plugin-owned mcp keys (${ownedKeys.join(', ')})`,
318
+ executed: false,
319
+ });
320
+ }
321
+ writeJournal(journalPath, journal); // write-ahead: plan on disk BEFORE deleting
322
+ }
323
+ const removeMcpKeys = io?.removeMcpKeys
324
+ ?? (async (repoPath, keys) => {
325
+ await plugin_manager_1.PluginManager.removeMcpServers(repoPath, keys, 'claude');
326
+ });
327
+ for (const entry of journal.entries) {
328
+ if (entry.executed)
329
+ continue;
330
+ try {
331
+ if (entry.action === 'move' && entry.dest) {
332
+ if (fs.existsSync(entry.src))
333
+ moveOverwriting(entry.src, entry.dest);
334
+ }
335
+ else if (entry.action === 'delete') {
336
+ if (isProtectedPath(project.path, entry.src))
337
+ throw new Error(`protected path: ${entry.src}`);
338
+ fs.rmSync(entry.src, { recursive: true, force: true });
339
+ }
340
+ else if (entry.action === 'mcp-key-remove') {
341
+ const ownedKeys = plugins_1.BUNDLED_PLUGINS.flatMap((p) => p.manifest.owns.mcpServers ?? []);
342
+ await removeMcpKeys(project.path, ownedKeys);
343
+ }
344
+ entry.executed = true;
345
+ writeJournal(journalPath, journal);
346
+ }
347
+ catch (err) {
348
+ // Fail-open: abort the remaining cleanup for this project. State already
349
+ // moved keeps working (the activation gate prefers the workspace).
350
+ entry.error = err instanceof Error ? err.message : String(err);
351
+ writeJournal(journalPath, journal);
352
+ return {
353
+ projectId: project.id,
354
+ migrated: false,
355
+ error: `migration halted at ${entry.action} ${entry.src}: ${entry.error}`,
356
+ journalPath,
357
+ };
358
+ }
359
+ }
360
+ journal.finishedAt = new Date().toISOString();
361
+ writeJournal(journalPath, journal);
362
+ return { projectId: project.id, migrated: true, journalPath };
363
+ }
364
+ // ─── Startup sweep ───────────────────────────────────────────────────────────
365
+ /**
366
+ * Serialized background sweep over registered projects. Never throws; each
367
+ * project's outcome is logged, failures surface once as a console warning
368
+ * (non-blocking by design — the project keeps working either way).
369
+ */
370
+ async function runLegacyMigrationSweep(projects, io) {
371
+ if (!isLegacyMigrationEnabled())
372
+ return [];
373
+ const results = [];
374
+ for (const project of projects) {
375
+ try {
376
+ const result = await migrateProject(project, io);
377
+ results.push(result);
378
+ if (result.migrated) {
379
+ console.log(`[legacy-migration] migrated ${project.slug} (journal: ${result.journalPath})`);
380
+ }
381
+ else if (result.error) {
382
+ console.warn(`[legacy-migration] ${project.slug}: ${result.error}`);
383
+ }
384
+ }
385
+ catch (err) {
386
+ results.push({
387
+ projectId: project.id,
388
+ migrated: false,
389
+ error: err instanceof Error ? err.message : String(err),
390
+ });
391
+ console.warn(`[legacy-migration] ${project.slug} threw:`, err);
392
+ }
393
+ }
394
+ return results;
395
+ }
@@ -22,6 +22,7 @@ const spawn_lifecycle_1 = require("./spawn-lifecycle");
22
22
  const result_event_1 = require("./result-event");
23
23
  const loop_decider_1 = require("./loop-decider");
24
24
  const rail_isolation_1 = require("./rail-isolation");
25
+ const repo_map_1 = require("./repo-map");
25
26
  const feature_flags_1 = require("./feature-flags");
26
27
  const runtime_1 = require("./providers/runtime");
27
28
  // Per-step wall-clock caps so a single hung step can't block the engine's
@@ -95,7 +96,10 @@ function runShellCommand(command, cwd, env, timeoutMs, onLine, onSpawn) {
95
96
  */
96
97
  function aiStepEnv(env, repoDir) {
97
98
  const base = repoDir ? { ...env, SPECRAILS_REPO_DIR: repoDir } : env;
98
- return (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? { ...base, SPECRAILS_GIT_AUTO: 'false' } : base;
99
+ // Deterministic repo map (zero-AI) so the pipeline's exploration phase
100
+ // starts oriented instead of spending its first turns on `ls`/`find`.
101
+ const withMap = (0, repo_map_1.injectRepoMapEnv)(base, repoDir);
102
+ return (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? { ...withMap, SPECRAILS_GIT_AUTO: 'false' } : withMap;
99
103
  }
100
104
  /**
101
105
  * Relocated cwd is the workspace; the source repo is reached via the
@@ -144,7 +144,8 @@ function defaultModelForProvider(opts, provider) {
144
144
  * from the npx fallback in development/runtimes-less builds: registry mirror
145
145
  * → framework materialize (idempotent) → one `init` per provider. Throws on
146
146
  * any failure — the caller surfaces it as a failed step; nothing here registers
147
- * the project.
147
+ * the project. With `continueOnError` it instead reports per-provider results
148
+ * (silent-add mode).
148
149
  */
149
150
  async function assembleProjectOffline(opts) {
150
151
  const { projectPath, slug, desktopProjectId, providers } = opts;
@@ -183,7 +184,8 @@ async function assembleProjectOffline(opts) {
183
184
  materialize(providers);
184
185
  const spawnInit = opts.io?.spawnInit ?? (useBundled ? spawnBundledCoreInit : spawnNpxCoreInit);
185
186
  const coreLabel = useBundled ? 'bundled core' : 'npx core';
186
- for (const provider of providers) {
187
+ const results = [];
188
+ const assembleOne = async (provider) => {
187
189
  const configPath = (0, install_config_path_1.installConfigPathForProvider)({ slug, path: projectPath }, provider);
188
190
  (0, fs_1.mkdirSync)((0, path_1.dirname)(configPath), { recursive: true });
189
191
  (0, fs_1.writeFileSync)(configPath, buildQuickInstallConfigYaml(provider, defaultModelForProvider(opts, provider)), 'utf-8');
@@ -204,12 +206,38 @@ async function assembleProjectOffline(opts) {
204
206
  const tail = stderrChunks.join('').split('\n').filter(Boolean).slice(-5).join(' ').slice(0, 500);
205
207
  throw new Error(`${coreLabel} init failed for ${provider} (exit ${code})${tail ? `: ${tail}` : ''}`);
206
208
  }
209
+ };
210
+ for (const provider of providers) {
211
+ opts.onProviderStart?.(provider);
212
+ try {
213
+ await assembleOne(provider);
214
+ const result = { provider, ok: true };
215
+ results.push(result);
216
+ opts.onProviderResult?.(result);
217
+ }
218
+ catch (err) {
219
+ const result = {
220
+ provider,
221
+ ok: false,
222
+ error: err instanceof Error ? err.message : String(err),
223
+ };
224
+ results.push(result);
225
+ opts.onProviderResult?.(result);
226
+ if (!opts.continueOnError)
227
+ throw err;
228
+ }
207
229
  }
208
230
  // The activation gate (workspace-resolution) requires a populated workspace;
209
231
  // verify assemble actually produced one so a silent core no-op cannot leave
210
- // a half-created project that would spawn from a repo cwd.
232
+ // a half-created project that would spawn from a repo cwd. In
233
+ // continueOnError mode this only applies when at least one provider
234
+ // succeeded (an all-failed run already carries its per-provider errors).
211
235
  const workspace = (0, workspace_manager_1.workspacePathFor)(slug);
212
- if (!(0, fs_1.existsSync)(workspace)) {
236
+ if (!(0, fs_1.existsSync)(workspace) && results.some((r) => r.ok)) {
237
+ throw new Error(`assemble completed but workspace was not created at ${workspace}`);
238
+ }
239
+ if (!opts.continueOnError && !(0, fs_1.existsSync)(workspace)) {
213
240
  throw new Error(`assemble completed but workspace was not created at ${workspace}`);
214
241
  }
242
+ return results;
215
243
  }
@@ -79,8 +79,21 @@ function buildCanonicalPrBody(input) {
79
79
  const changesSection = buildChangesSection(input.changes ?? null);
80
80
  if (changesSection)
81
81
  parts.push(changesSection);
82
+ const costLine = buildCostFooter(input.costUsd ?? null);
83
+ if (costLine)
84
+ parts.push(costLine);
82
85
  return parts.join('\n\n');
83
86
  }
87
+ /** `> Specrails cost: $X.XX` — the transparency footer that keeps AI spend
88
+ * visible at the exact place the work is reviewed. `~` marks a total that
89
+ * includes rate-card estimates (providers without native cost, kill-path
90
+ * reconstructions). */
91
+ function buildCostFooter(cost) {
92
+ if (!cost || !Number.isFinite(cost.totalUsd) || cost.totalUsd <= 0)
93
+ return null;
94
+ const prefix = cost.estimated ? '~' : '';
95
+ return `> Specrails cost: ${prefix}$${cost.totalUsd.toFixed(2)}`;
96
+ }
84
97
  function summaryParagraph(input) {
85
98
  const n = input.tickets.length;
86
99
  const refs = input.tickets.map((t) => displayRef(t)).join(', ');
@@ -10,6 +10,7 @@ const db_1 = require("./db");
10
10
  const queue_manager_1 = require("./queue-manager");
11
11
  const feature_flags_1 = require("./feature-flags");
12
12
  const types_1 = require("./types");
13
+ const job_phase_breakdown_1 = require("./job-phase-breakdown");
13
14
  const providers_1 = require("./providers");
14
15
  const hooks_1 = require("./hooks");
15
16
  const explore_smash_1 = require("./explore-smash");
@@ -589,6 +590,21 @@ function registerJobsRoutes(deps) {
589
590
  res.json({ jobs });
590
591
  }
591
592
  });
593
+ // Per-phase cost/usage attribution, computed post-hoc from the persisted
594
+ // raw stream events (claude Task/parent_tool_use_id envelope). Costs are
595
+ // rate-card ESTIMATES and flagged as such; providers whose stream carries
596
+ // no per-event usage (codex/gemini/kimi) return breakdown: null.
597
+ router.get('/:projectId/jobs/:id/phase-breakdown', (req, res) => {
598
+ const { db } = ctx(req);
599
+ const job = db.prepare('SELECT id FROM jobs WHERE id = ?').get(req.params.id);
600
+ if (!job) {
601
+ res.status(404).json({ error: 'Job not found' });
602
+ return;
603
+ }
604
+ const events = db.prepare("SELECT event_type, payload, timestamp FROM events WHERE job_id = ? AND event_type IN ('assistant', 'user') ORDER BY seq ASC").all(req.params.id);
605
+ const breakdown = (0, job_phase_breakdown_1.computeJobPhaseBreakdown)(events);
606
+ res.json({ jobId: req.params.id, breakdown });
607
+ });
592
608
  // Must be registered BEFORE /:projectId/jobs/:id, otherwise Express matches
593
609
  // the parameterized route first with id='compare' and this never runs (the
594
610
  // Job Comparison feature would always 404).