specrails-desktop 2.36.0 → 2.42.2

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 (510) hide show
  1. package/README.md +94 -247
  2. package/client/dist/assets/ActivityFeedPage-DoEMumQS.js +1 -0
  3. package/client/dist/assets/AgentBrowserCapture-DIh4K0gh.js +1 -0
  4. package/client/dist/assets/AgentModeAnalyticsPane-mbq4WEoO.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-CKh7zuA4.js +2 -0
  6. package/client/dist/assets/AgentModeJobsPane-BDokPiLJ.js +1 -0
  7. package/client/dist/assets/AgentsPage-BFVAZbBj.js +87 -0
  8. package/client/dist/assets/AnalyticsPage-3Y9LuWwz.js +1 -0
  9. package/client/dist/assets/CodePage-C9KyuV_C.js +3 -0
  10. package/client/dist/assets/DesktopAnalyticsPage-Bhi54bXQ.js +1 -0
  11. package/client/dist/assets/{DocsDialog-C76VdBJC.js → DocsDialog-Dj6mt4ga.js} +2 -2
  12. package/client/dist/assets/{DocsPage-ziMlj6B6.js → DocsPage-24yWglNI.js} +1 -1
  13. package/client/dist/assets/ExportDropdown-ophapHrT.js +1 -0
  14. package/client/dist/assets/InteractiveJobComposer-C1ucmxQh.js +19 -0
  15. package/client/dist/assets/JobDetailModal-DNWmgVHD.js +1 -0
  16. package/client/dist/assets/JobDetailPage-BcajZyuu.js +1 -0
  17. package/client/dist/assets/JobsPage-B9nTZw6e.js +1 -0
  18. package/client/dist/assets/LoopBuilderPage-Oc48iOFW.js +7 -0
  19. package/client/dist/assets/LoopPreviewModal-ae0Fr6SU.js +1 -0
  20. package/client/dist/assets/LoopsPage-CUEqzUBI.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext-Deg39peL.js +1 -0
  22. package/client/dist/assets/PluginsPage-C1xtARIx.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-JZzrXFBN.js +1 -0
  24. package/client/dist/assets/RepositoryDeliveries-CSDsiVUz.js +5 -0
  25. package/client/dist/assets/RepositoryScopeSelector-D-RCaknl.js +1 -0
  26. package/client/dist/assets/ReviewPacketPage-x5EfGTiL.js +1 -0
  27. package/client/dist/assets/TemplatePreviewModal-BSyuZxGq.js +1 -0
  28. package/client/dist/assets/TicketDetailModalContext-DqyF0so-.js +14 -0
  29. package/client/dist/assets/Trans-SnMX4MhW.js +1 -0
  30. package/client/dist/assets/agent-BdT0SB1D.js +1 -0
  31. package/client/dist/assets/agent-Bh9K7iEt.js +1 -0
  32. package/client/dist/assets/agent-C4ibdFk9.js +1 -0
  33. package/client/dist/assets/agent-CSFY22Zo.js +1 -0
  34. package/client/dist/assets/agent-DAnqxUn8.js +1 -0
  35. package/client/dist/assets/agent-DTQiu0Mi.js +1 -0
  36. package/client/dist/assets/agent-T54WuR_Y.js +1 -0
  37. package/client/dist/assets/agent-i75WDZOA.js +1 -0
  38. package/client/dist/assets/browser-05vRDg_y.js +1 -0
  39. package/client/dist/assets/browser-Bulb-cB1.js +1 -0
  40. package/client/dist/assets/browser-C7f-Jsg1.js +1 -0
  41. package/client/dist/assets/browser-CG0PE1j_.js +1 -0
  42. package/client/dist/assets/browser-CMzaqAjo.js +1 -0
  43. package/client/dist/assets/browser-DMajYdNX.js +1 -0
  44. package/client/dist/assets/browser-DxcOHrdP.js +1 -0
  45. package/client/dist/assets/browser-ObE7xVo3.js +1 -0
  46. package/client/dist/assets/builder-765NeYTp.js +1 -0
  47. package/client/dist/assets/builder-8MG3V6ea.js +1 -0
  48. package/client/dist/assets/builder-B-1UBs5K.js +1 -0
  49. package/client/dist/assets/builder-BTiGnPT0.js +1 -0
  50. package/client/dist/assets/builder-Cx9Rz-7u.js +1 -0
  51. package/client/dist/assets/builder-DKFMELBl.js +1 -0
  52. package/client/dist/assets/builder-OcmKrC0h.js +1 -0
  53. package/client/dist/assets/builder-P9qVBXFZ.js +1 -0
  54. package/client/dist/assets/{button-CJqFnUOL.js → button-DCBqEaZl.js} +1 -1
  55. package/client/dist/assets/{code-CkUuW4m9.js → code-B37GRg9C.js} +1 -1
  56. package/client/dist/assets/{code-B0O8-p8k.js → code-BBOEZbf9.js} +1 -1
  57. package/client/dist/assets/{code-B1VLZDhS.js → code-BRm-jcyx.js} +1 -1
  58. package/client/dist/assets/{code-Bpj4Ddml.js → code-CF7w62CR.js} +1 -1
  59. package/client/dist/assets/{code-BykBZ5-e.js → code-CLuvJt2r.js} +1 -1
  60. package/client/dist/assets/{code-B86IMAfz.js → code-D0RCm59f.js} +1 -1
  61. package/client/dist/assets/code-DaZ-ZwCT.js +1 -0
  62. package/client/dist/assets/{code-cAqaqMTx.js → code-iIHkGaCL.js} +1 -1
  63. package/client/dist/assets/commands-0XjAftla.js +1 -0
  64. package/client/dist/assets/commands-BAWSYHjQ.js +1 -0
  65. package/client/dist/assets/commands-BBwowgiE.js +1 -0
  66. package/client/dist/assets/commands-BacjIZLN.js +1 -0
  67. package/client/dist/assets/commands-Bs62IAZ3.js +1 -0
  68. package/client/dist/assets/commands-CLPdh-Hh.js +1 -0
  69. package/client/dist/assets/commands-DlbFzgrp.js +1 -0
  70. package/client/dist/assets/commands-LjPSsi5F.js +1 -0
  71. package/client/dist/assets/common-B5AZNYMH.js +1 -0
  72. package/client/dist/assets/common-BFZ0c_vN.js +1 -0
  73. package/client/dist/assets/common-BrlwEnL7.js +1 -0
  74. package/client/dist/assets/common-BttQx3z4.js +1 -0
  75. package/client/dist/assets/common-CM65k_-e.js +1 -0
  76. package/client/dist/assets/common-D4mvFFlu.js +1 -0
  77. package/client/dist/assets/common-TZ9Ny96S.js +1 -0
  78. package/client/dist/assets/common-nFpSF4GC.js +1 -0
  79. package/client/dist/assets/dashboard-B14uYXkG.js +1 -0
  80. package/client/dist/assets/dashboard-BK_mez-z.js +1 -0
  81. package/client/dist/assets/dashboard-BfBir_6o.js +1 -0
  82. package/client/dist/assets/dashboard-Bt0cCzeG.js +1 -0
  83. package/client/dist/assets/dashboard-BwkjxI6H.js +1 -0
  84. package/client/dist/assets/{dashboard-C8WOEiPu.js → dashboard-C3bDKf7s.js} +1 -1
  85. package/client/dist/assets/dashboard-CdvjGfog.js +1 -0
  86. package/client/dist/assets/dashboard-fRrP0Kv0.js +1 -0
  87. package/client/dist/assets/dialog-SRw01_QU.js +45 -0
  88. package/client/dist/assets/dist-js-CJ_2XX1Y.js +1 -0
  89. package/client/dist/assets/formatDistanceToNow-DSj5DprF.js +1 -0
  90. package/client/dist/assets/getTimezoneOffsetInMilliseconds-YhD8rIEs.js +1 -0
  91. package/client/dist/assets/index-DwAmL7m5.css +2 -0
  92. package/client/dist/assets/index-EzrkUb8B.js +76 -0
  93. package/client/dist/assets/{jira-api-B1-MUehg.js → jira-api-CSCqGr-Q.js} +1 -1
  94. package/client/dist/assets/{jobs-CN_zv4Gr.js → jobs-B8rrzfyZ.js} +1 -1
  95. package/client/dist/assets/{jobs-DUlWSYq2.js → jobs-BS6RTurf.js} +1 -1
  96. package/client/dist/assets/{jobs-Bpks6doU.js → jobs-BTT0Bsks.js} +1 -1
  97. package/client/dist/assets/{jobs-Bgq-PZkC.js → jobs-CezU69AE.js} +1 -1
  98. package/client/dist/assets/{jobs-SwYS2M-B.js → jobs-ClFJae0s.js} +1 -1
  99. package/client/dist/assets/{jobs-BAls3bWq.js → jobs-DJSFIFVy.js} +1 -1
  100. package/client/dist/assets/{jobs-DhxKgWFq.js → jobs-DhgGr8vm.js} +1 -1
  101. package/client/dist/assets/{jobs-B_TeSQxb.js → jobs-DzMjUcaN.js} +1 -1
  102. package/client/dist/assets/mcp-6rMoZfEO.js +1 -0
  103. package/client/dist/assets/mcp-BD8sL5mP.js +1 -0
  104. package/client/dist/assets/mcp-BkcA3I5s.js +1 -0
  105. package/client/dist/assets/mcp-CCpSelOh.js +1 -0
  106. package/client/dist/assets/mcp-CUAN6ORK.js +1 -0
  107. package/client/dist/assets/mcp-CqbRxRTE.js +1 -0
  108. package/client/dist/assets/mcp-DP2YfVhO.js +1 -0
  109. package/client/dist/assets/mcp-DUVQamDz.js +1 -0
  110. package/client/dist/assets/narration-B-ynWTpn.js +1 -0
  111. package/client/dist/assets/narration-Bj_laZwg.js +1 -0
  112. package/client/dist/assets/narration-BxHpmMv_.js +1 -0
  113. package/client/dist/assets/narration-C1Px0-0j.js +1 -0
  114. package/client/dist/assets/narration-CAXhGbzM.js +1 -0
  115. package/client/dist/assets/narration-DQ1l_Kq6.js +1 -0
  116. package/client/dist/assets/narration-XISXwqZd.js +1 -0
  117. package/client/dist/assets/narration-kwzfglQo.js +1 -0
  118. package/client/dist/assets/nav-1-Hh2kOz.js +1 -0
  119. package/client/dist/assets/nav-B34QkESm.js +1 -0
  120. package/client/dist/assets/nav-Bap_UXAq.js +1 -0
  121. package/client/dist/assets/{nav-pYuXq7sf.js → nav-BwxXIB24.js} +1 -1
  122. package/client/dist/assets/nav-Ch-zHBG5.js +1 -0
  123. package/client/dist/assets/nav-CygC_WN-.js +1 -0
  124. package/client/dist/assets/{nav-iwjCg5FR.js → nav-KnnLQtOQ.js} +1 -1
  125. package/client/dist/assets/nav-m00uxYUG.js +1 -0
  126. package/client/dist/assets/packet-BQ2tXfJI.js +1 -0
  127. package/client/dist/assets/packet-BdpOJ1Cy.js +1 -0
  128. package/client/dist/assets/packet-C3Ievb9R.js +1 -0
  129. package/client/dist/assets/packet-C4zJIG0Y.js +1 -0
  130. package/client/dist/assets/packet-Cx-zRuWn.js +1 -0
  131. package/client/dist/assets/packet-DUTGX52M.js +1 -0
  132. package/client/dist/assets/packet-DhsKtUPB.js +1 -0
  133. package/client/dist/assets/packet-z3LJ8Zyy.js +1 -0
  134. package/client/dist/assets/project-repositories-DYhTvwQ1.js +1 -0
  135. package/client/dist/assets/provider-capabilities-B-wOHfLP.js +1 -0
  136. package/client/dist/assets/settings-BBt2xtDQ.js +1 -0
  137. package/client/dist/assets/settings-BJB9RKJj.js +1 -0
  138. package/client/dist/assets/settings-CHwQR2rB.js +1 -0
  139. package/client/dist/assets/settings-CrMi3kSi.js +1 -0
  140. package/client/dist/assets/settings-DKyhDGD1.js +1 -0
  141. package/client/dist/assets/settings-Deupji9c.js +1 -0
  142. package/client/dist/assets/settings-DqRMmwAM.js +1 -0
  143. package/client/dist/assets/settings-J4rj6vo-.js +1 -0
  144. package/client/dist/assets/setup-2xGgu4Sk.js +1 -0
  145. package/client/dist/assets/setup-Ba11l2JA.js +1 -0
  146. package/client/dist/assets/setup-BzW4kMok.js +1 -0
  147. package/client/dist/assets/setup-COzwYhkJ.js +1 -0
  148. package/client/dist/assets/setup-CRVULhtF.js +1 -0
  149. package/client/dist/assets/setup-DgvlxwpE.js +1 -0
  150. package/client/dist/assets/setup-Dw_wYeEY.js +1 -0
  151. package/client/dist/assets/setup-ZPtEeJPW.js +1 -0
  152. package/client/dist/assets/spending-BqbdPm_4.js +1 -0
  153. package/client/dist/assets/square-Ci-PFcFe.js +1 -0
  154. package/client/dist/assets/{terminal-BFRZnMPP.js → terminal-BAzpHtpY.js} +1 -1
  155. package/client/dist/assets/{terminal-IahE4LW9.js → terminal-C7dHeR3x.js} +1 -1
  156. package/client/dist/assets/{terminal-tITa81iz.js → terminal-CMla5Qsz.js} +1 -1
  157. package/client/dist/assets/{terminal-BZiIu3rf.js → terminal-CdNXH-n9.js} +1 -1
  158. package/client/dist/assets/{terminal-ScjyXNxQ.js → terminal-Cw5bZsmV.js} +1 -1
  159. package/client/dist/assets/{terminal-C2NAd7BW.js → terminal-DFUP4eNv.js} +1 -1
  160. package/client/dist/assets/{terminal-d7GadXbN.js → terminal-DGqljeK_.js} +1 -1
  161. package/client/dist/assets/{terminal-QpXZU2Bn.js → terminal-cU70NodO.js} +1 -1
  162. package/client/dist/assets/useDesktop-RSMTyy7V.js +1 -0
  163. package/client/dist/assets/useSharedWebSocket-BOdS5_Iz.js +2 -0
  164. package/client/dist/index.html +34 -35
  165. package/docs/README.md +3 -0
  166. package/docs/agent-live-steering.md +57 -0
  167. package/docs/ci-cd.md +57 -0
  168. package/docs/code-explorer.md +48 -0
  169. package/docs/codex.md +12 -1
  170. package/docs/features/detachable-mission-windows.md +59 -0
  171. package/docs/guide/de/integrations/5-mcp-server.md +13 -9
  172. package/docs/guide/de/integrations/6-agent-chat.md +1 -0
  173. package/docs/guide/en/integrations/5-mcp-server.md +13 -9
  174. package/docs/guide/en/integrations/6-agent-chat.md +1 -0
  175. package/docs/guide/es/integrations/5-mcp-server.md +13 -9
  176. package/docs/guide/es/integrations/6-agent-chat.md +1 -0
  177. package/docs/guide/fr/integrations/5-mcp-server.md +13 -9
  178. package/docs/guide/fr/integrations/6-agent-chat.md +1 -0
  179. package/docs/guide/it/integrations/5-mcp-server.md +13 -9
  180. package/docs/guide/it/integrations/6-agent-chat.md +1 -0
  181. package/docs/guide/ja/integrations/5-mcp-server.md +13 -9
  182. package/docs/guide/ja/integrations/6-agent-chat.md +1 -0
  183. package/docs/guide/pt/integrations/5-mcp-server.md +13 -9
  184. package/docs/guide/pt/integrations/6-agent-chat.md +1 -0
  185. package/docs/guide/zh/integrations/5-mcp-server.md +13 -9
  186. package/docs/guide/zh/integrations/6-agent-chat.md +1 -0
  187. package/docs/internals/README.md +1 -0
  188. package/docs/internals/browser-capture-performance.md +55 -3
  189. package/docs/internals/browser-login-popups.md +45 -0
  190. package/docs/internals/browser-native-retina-audit.md +39 -0
  191. package/docs/internals/ci-cd-audit.md +39 -0
  192. package/docs/internals/core-runtime-updates.md +68 -0
  193. package/docs/internals/embedded-browser-native-webview-evaluation.md +2 -0
  194. package/docs/internals/implementation-reliability-audit.md +93 -0
  195. package/docs/internals/interactive-jobs.md +110 -6
  196. package/docs/internals/mcp-mission-audit.md +76 -0
  197. package/docs/internals/project-builder.md +404 -42
  198. package/docs/internals/safe-pr-review-flow.md +52 -10
  199. package/docs/internals/startup-recovery-audit.md +58 -0
  200. package/docs/mcp.md +74 -41
  201. package/docs/mission-processes.md +59 -0
  202. package/docs/multi-repo-projects.md +51 -0
  203. package/docs/platforms/windows-parity.md +70 -0
  204. package/docs/platforms/windows.md +13 -11
  205. package/docs/running-pipelines.md +14 -0
  206. package/mcp-bridge/dist/specrails-mcp.js +7186 -0
  207. package/package.json +9 -4
  208. package/server/dist/agent-chat-manager.js +537 -68
  209. package/server/dist/agent-chat-router.js +87 -6
  210. package/server/dist/agent-context-resolver.js +129 -25
  211. package/server/dist/agent-defaults.js +19 -11
  212. package/server/dist/agent-input-store.js +206 -0
  213. package/server/dist/agent-mcp-config.js +54 -15
  214. package/server/dist/agent-operator-prompt.js +255 -65
  215. package/server/dist/agent-spec-framing.js +202 -0
  216. package/server/dist/agent-steering.js +218 -0
  217. package/server/dist/agent-store.js +153 -0
  218. package/server/dist/api-not-found.js +8 -0
  219. package/server/dist/background-process-control.js +164 -0
  220. package/server/dist/background-process-service.js +61 -0
  221. package/server/dist/background-process-store.js +242 -0
  222. package/server/dist/background-windows-bootstrap.js +238 -0
  223. package/server/dist/blueprint-chat-manager.js +476 -62
  224. package/server/dist/blueprint-commit.js +21 -1
  225. package/server/dist/blueprint-draft-parser.js +122 -17
  226. package/server/dist/blueprint-generation.js +238 -0
  227. package/server/dist/blueprint-operator-prompt.js +91 -63
  228. package/server/dist/blueprint-router.js +55 -3
  229. package/server/dist/blueprint-spec-fixtures.js +85 -0
  230. package/server/dist/blueprint-spec-quality.js +57 -16
  231. package/server/dist/blueprint-store.js +92 -1
  232. package/server/dist/browser-capture-manager.js +86 -30
  233. package/server/dist/browser-playwright.js +168 -45
  234. package/server/dist/browser-viewport.js +26 -0
  235. package/server/dist/chat-manager.js +23 -15
  236. package/server/dist/chromium-archive.cjs +83 -0
  237. package/server/dist/chromium-resolver.js +72 -64
  238. package/server/dist/code-activity.js +145 -0
  239. package/server/dist/code-explorer-router.js +714 -114
  240. package/server/dist/core-compat.js +28 -1
  241. package/server/dist/core-execution.js +111 -0
  242. package/server/dist/core-package.js +4 -16
  243. package/server/dist/core-runtime.js +202 -0
  244. package/server/dist/core-update-manager.js +161 -60
  245. package/server/dist/core-update-state.js +29 -0
  246. package/server/dist/db.js +66 -35
  247. package/server/dist/delivery-evidence.js +142 -39
  248. package/server/dist/desktop-db.js +239 -43
  249. package/server/dist/desktop-router.js +109 -9
  250. package/server/dist/dev-ports.js +8 -3
  251. package/server/dist/file-provenance.js +34 -22
  252. package/server/dist/file-story-manager.js +62 -64
  253. package/server/dist/file-story.js +118 -35
  254. package/server/dist/file-summary-generator.js +46 -5
  255. package/server/dist/file-summary-manager.js +355 -106
  256. package/server/dist/framework-manager.js +98 -110
  257. package/server/dist/framework-reseed.js +76 -10
  258. package/server/dist/git-diagnostics.js +2 -2
  259. package/server/dist/headroom-manager.js +38 -3
  260. package/server/dist/host-control.js +52 -0
  261. package/server/dist/index.js +43 -2
  262. package/server/dist/interactive-job-session.js +149 -16
  263. package/server/dist/internal-api.js +48 -0
  264. package/server/dist/jira/jira-materializer.js +1 -0
  265. package/server/dist/json-tolerant.js +174 -0
  266. package/server/dist/legacy-migration.js +4 -2
  267. package/server/dist/loop-command-catalog.js +83 -21
  268. package/server/dist/loop-constants.js +6 -1
  269. package/server/dist/loop-decider.js +11 -1
  270. package/server/dist/loop-executors.js +194 -45
  271. package/server/dist/loop-factory.js +11 -4
  272. package/server/dist/loop-graph.js +90 -4
  273. package/server/dist/loop-run-manager.js +301 -69
  274. package/server/dist/loop-shell-invocation.js +21 -0
  275. package/server/dist/loop-step-idle.js +55 -0
  276. package/server/dist/loop-templates.js +31 -7
  277. package/server/dist/mcp/agent-capability.js +37 -4
  278. package/server/dist/mcp/guide.js +149 -36
  279. package/server/dist/mcp/mcp-admin-router.js +20 -4
  280. package/server/dist/mcp/mcp-server.js +89 -43
  281. package/server/dist/mcp/mcp-tiers.js +6 -5
  282. package/server/dist/mcp/mcp-token.js +3 -1
  283. package/server/dist/mcp/resources.js +5 -4
  284. package/server/dist/mcp/tools/catalog.js +4 -0
  285. package/server/dist/mcp/tools/code.js +125 -14
  286. package/server/dist/mcp/tools/context.js +153 -0
  287. package/server/dist/mcp/tools/git.js +15 -5
  288. package/server/dist/mcp/tools/jobs.js +84 -71
  289. package/server/dist/mcp/tools/loops.js +17 -6
  290. package/server/dist/mcp/tools/meta.js +74 -27
  291. package/server/dist/mcp/tools/mission.js +29 -0
  292. package/server/dist/mcp/tools/projects.js +22 -6
  293. package/server/dist/mcp/tools/rails.js +48 -11
  294. package/server/dist/mcp/tools/specs.js +86 -14
  295. package/server/dist/mcp/tools/support.js +1 -1
  296. package/server/dist/mcp/tools/types.js +117 -43
  297. package/server/dist/mcp/tools/watch.js +144 -51
  298. package/server/dist/milestone-chain-store.js +164 -0
  299. package/server/dist/milestone-chain.js +487 -0
  300. package/server/dist/milestone-progress.js +348 -0
  301. package/server/dist/mobile/mobile-devices.js +10 -7
  302. package/server/dist/mobile/mobile-gateway.js +2 -0
  303. package/server/dist/mobile/mobile-missions.js +214 -0
  304. package/server/dist/mobile/mobile-redact.js +17 -3
  305. package/server/dist/mobile/mobile-router.js +46 -4
  306. package/server/dist/mobile/mobile-ws.js +49 -4
  307. package/server/dist/multi-repo-bases.js +56 -0
  308. package/server/dist/multi-repo-checkout.js +101 -0
  309. package/server/dist/multi-repo-delivery.js +100 -0
  310. package/server/dist/multi-repo-execution-store.js +65 -0
  311. package/server/dist/multi-repo-execution.js +453 -0
  312. package/server/dist/offline-assemble.js +10 -8
  313. package/server/dist/openspec-runtime-plugin-commands.json +18 -0
  314. package/server/dist/openspec-runtime-plugin.js +120 -0
  315. package/server/dist/plugins/codex-spawn.js +49 -0
  316. package/server/dist/plugins/serena/manifest.js +2 -2
  317. package/server/dist/plugins/serena/templates/instructions.md +16 -0
  318. package/server/dist/pr-publisher.js +1 -1
  319. package/server/dist/profiles-router.js +3 -18
  320. package/server/dist/project-code-discovery.js +142 -0
  321. package/server/dist/project-git.js +19 -21
  322. package/server/dist/project-registry.js +272 -20
  323. package/server/dist/project-repositories.js +157 -0
  324. package/server/dist/project-repository-provenance.js +31 -0
  325. package/server/dist/project-router-background-processes.js +105 -0
  326. package/server/dist/project-router-git.js +75 -16
  327. package/server/dist/project-router-jobs.js +2 -126
  328. package/server/dist/project-router-loop-runs.js +72 -2
  329. package/server/dist/project-router-tickets.js +53 -8
  330. package/server/dist/project-router.js +239 -38
  331. package/server/dist/provider-detection.js +18 -6
  332. package/server/dist/provider-limit.js +64 -0
  333. package/server/dist/providers/claude-adapter.js +26 -1
  334. package/server/dist/providers/claude-background-tasks.js +18 -0
  335. package/server/dist/providers/claude-live-session.js +383 -0
  336. package/server/dist/providers/codex-adapter.js +19 -0
  337. package/server/dist/providers/codex-live-session.js +545 -0
  338. package/server/dist/providers/gemini-adapter.js +1 -0
  339. package/server/dist/providers/live-session-types.js +12 -0
  340. package/server/dist/providers/live-session.js +13 -0
  341. package/server/dist/providers/terminal-result.js +24 -0
  342. package/server/dist/queue-manager.js +2 -0
  343. package/server/dist/rail-isolated-launch.js +141 -16
  344. package/server/dist/rail-pr-decision.js +364 -99
  345. package/server/dist/rail-pr-store.js +24 -10
  346. package/server/dist/rail-worktree-release.js +34 -2
  347. package/server/dist/rail-worktrees-store.js +4 -4
  348. package/server/dist/rails-router.js +249 -55
  349. package/server/dist/repo-lock.js +92 -17
  350. package/server/dist/review-packet.js +23 -13
  351. package/server/dist/schemas/file-summary.v1.json +59 -59
  352. package/server/dist/schemas/profile.v1.json +137 -137
  353. package/server/dist/setup-manager.js +52 -8
  354. package/server/dist/shell-integration/bash-shim.bash +45 -0
  355. package/server/dist/shell-integration/fish-shim.fish +36 -0
  356. package/server/dist/shell-integration/powershell-shim.ps1 +48 -0
  357. package/server/dist/shell-integration/zsh-shim.zsh +47 -0
  358. package/server/dist/smash-runner.js +1 -0
  359. package/server/dist/spawn-lifecycle.js +44 -27
  360. package/server/dist/spec-contract-prompt.js +143 -0
  361. package/server/dist/spec-draft-parser.js +6 -1
  362. package/server/dist/spec-models.js +1 -0
  363. package/server/dist/stuck-run-detector.js +1 -0
  364. package/server/dist/terminal-manager.js +21 -1
  365. package/server/dist/terminal-shell-integration.js +4 -1
  366. package/server/dist/transient-children.js +619 -230
  367. package/server/dist/util/cli-prompt.js +78 -5
  368. package/server/dist/util/sqlite-migrations.js +20 -0
  369. package/server/dist/util/win-spawn.js +18 -3
  370. package/server/dist/util/windows-provider-shell.js +29 -0
  371. package/server/dist/verification-sentinel.js +14 -0
  372. package/server/dist/vitest-setup.js +2 -0
  373. package/server/dist/windows-job-supervisor.js +133 -0
  374. package/server/dist/workspace-manager.js +9 -30
  375. package/server/dist/worktree-manager.js +17 -2
  376. package/server/dist/worktree-overlay.js +3 -1
  377. package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +0 -1
  378. package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +0 -1
  379. package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +0 -2
  380. package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +0 -2
  381. package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +0 -1
  382. package/client/dist/assets/AgentsPage-C8J_42sm.js +0 -87
  383. package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +0 -1
  384. package/client/dist/assets/CodePage-BZ2MwdCu.js +0 -2
  385. package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +0 -1
  386. package/client/dist/assets/ExportDropdown-BrtfWzEG.js +0 -1
  387. package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +0 -19
  388. package/client/dist/assets/JobDetailModal-NAJmuYIT.js +0 -1
  389. package/client/dist/assets/JobDetailPage-fE0ZycUX.js +0 -1
  390. package/client/dist/assets/JobsPage-z5M_HVJG.js +0 -1
  391. package/client/dist/assets/LoopBuilderPage-j1FQS47c.js +0 -7
  392. package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +0 -1
  393. package/client/dist/assets/LoopsPage-Ll7zORZb.js +0 -1
  394. package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +0 -1
  395. package/client/dist/assets/PluginsPage-DN4kGvJt.js +0 -2
  396. package/client/dist/assets/ProjectSettingsDialog-OgsgxjzF.js +0 -1
  397. package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +0 -1
  398. package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +0 -1
  399. package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +0 -14
  400. package/client/dist/assets/Trans-Bsz9_gyq.js +0 -1
  401. package/client/dist/assets/agent-BQgKv_2F.js +0 -1
  402. package/client/dist/assets/agent-C1NRDJqL.js +0 -1
  403. package/client/dist/assets/agent-CLNleiy6.js +0 -1
  404. package/client/dist/assets/agent-CmceArkQ.js +0 -1
  405. package/client/dist/assets/agent-D3UxQ3SR.js +0 -1
  406. package/client/dist/assets/agent-DB1JeE64.js +0 -1
  407. package/client/dist/assets/agent-DXkHcQ-D.js +0 -1
  408. package/client/dist/assets/agent-DjJJJ2M8.js +0 -1
  409. package/client/dist/assets/auth-D_5bQv5W.js +0 -1
  410. package/client/dist/assets/ban-E4EaZe3a.js +0 -1
  411. package/client/dist/assets/bot-DzuuuC5R.js +0 -1
  412. package/client/dist/assets/browser-BgqHDMXP.js +0 -1
  413. package/client/dist/assets/browser-CsKJtrpv.js +0 -1
  414. package/client/dist/assets/browser-Cu58EpZU.js +0 -1
  415. package/client/dist/assets/browser-CvXOJm-j.js +0 -1
  416. package/client/dist/assets/browser-DOrFh1c7.js +0 -1
  417. package/client/dist/assets/browser-DQv_Jo7x.js +0 -1
  418. package/client/dist/assets/browser-LPgxvJu2.js +0 -1
  419. package/client/dist/assets/browser-eL-3MIEI.js +0 -1
  420. package/client/dist/assets/builder-0IWtosUm.js +0 -1
  421. package/client/dist/assets/builder-Bf2N6XWn.js +0 -1
  422. package/client/dist/assets/builder-BnqaaBkT.js +0 -1
  423. package/client/dist/assets/builder-BxxfGgAO.js +0 -1
  424. package/client/dist/assets/builder-C5IIgWsq.js +0 -1
  425. package/client/dist/assets/builder-CHuahItu.js +0 -1
  426. package/client/dist/assets/builder-CjWosXL_.js +0 -1
  427. package/client/dist/assets/builder-CpCIOoKj.js +0 -1
  428. package/client/dist/assets/code-CHPkOkDS.js +0 -1
  429. package/client/dist/assets/commands-6tYQih5m.js +0 -1
  430. package/client/dist/assets/commands-BK2QinXN.js +0 -1
  431. package/client/dist/assets/commands-C2Nca76h.js +0 -1
  432. package/client/dist/assets/commands-CKdLihNX.js +0 -1
  433. package/client/dist/assets/commands-CYIBTAlc.js +0 -1
  434. package/client/dist/assets/commands-CjUtYJIM.js +0 -1
  435. package/client/dist/assets/commands-Cq_u_8y9.js +0 -1
  436. package/client/dist/assets/commands-D_ZHYQug.js +0 -1
  437. package/client/dist/assets/common-6MLbD-jM.js +0 -1
  438. package/client/dist/assets/common-BKGEu4rm.js +0 -1
  439. package/client/dist/assets/common-BNbnv8cE.js +0 -1
  440. package/client/dist/assets/common-BqGWtDbt.js +0 -1
  441. package/client/dist/assets/common-DBtTzeYm.js +0 -1
  442. package/client/dist/assets/common-DEW3Nr8n.js +0 -1
  443. package/client/dist/assets/common-DHsv9N5d.js +0 -1
  444. package/client/dist/assets/common-mkrEcW-Y.js +0 -1
  445. package/client/dist/assets/dashboard-B9GhC5ku.js +0 -1
  446. package/client/dist/assets/dashboard-CBnv6zOI.js +0 -1
  447. package/client/dist/assets/dashboard-CaP0fU87.js +0 -1
  448. package/client/dist/assets/dashboard-D4ZZ4x84.js +0 -1
  449. package/client/dist/assets/dashboard-DmuTQBX8.js +0 -1
  450. package/client/dist/assets/dashboard-DynLYj3d.js +0 -1
  451. package/client/dist/assets/dashboard-zj4gGFJ1.js +0 -1
  452. package/client/dist/assets/dialog-CdWCb02j.js +0 -45
  453. package/client/dist/assets/dist-bVqfYmby.js +0 -1
  454. package/client/dist/assets/dist-js-CWeUdf5Z.js +0 -1
  455. package/client/dist/assets/formatDistanceToNow-DQCvMgUc.js +0 -1
  456. package/client/dist/assets/getTimezoneOffsetInMilliseconds-C16cxSe1.js +0 -1
  457. package/client/dist/assets/git-refresh-wTW0rO0Z.js +0 -1
  458. package/client/dist/assets/i18n-hnX2DPmo.js +0 -2
  459. package/client/dist/assets/index-DIhMEAVw.js +0 -66
  460. package/client/dist/assets/index-DwygcU7s.css +0 -2
  461. package/client/dist/assets/mcp-B8X8wBLu.js +0 -1
  462. package/client/dist/assets/mcp-C2AojZZN.js +0 -1
  463. package/client/dist/assets/mcp-D8BT78_L.js +0 -1
  464. package/client/dist/assets/mcp-Df5RWBNk.js +0 -1
  465. package/client/dist/assets/mcp-Dj7BgMQW.js +0 -1
  466. package/client/dist/assets/mcp-Q7iT7rcz.js +0 -1
  467. package/client/dist/assets/mcp-YlvNvJbW.js +0 -1
  468. package/client/dist/assets/mcp-mKFpTSRb.js +0 -1
  469. package/client/dist/assets/narration-4Z2VUhZw.js +0 -1
  470. package/client/dist/assets/narration-BGuiEDj3.js +0 -1
  471. package/client/dist/assets/narration-BuSoqRmv.js +0 -1
  472. package/client/dist/assets/narration-CB9t29fy.js +0 -1
  473. package/client/dist/assets/narration-CU3x8FHD.js +0 -1
  474. package/client/dist/assets/narration-CWeXhfP5.js +0 -1
  475. package/client/dist/assets/narration-DRaDnhT5.js +0 -1
  476. package/client/dist/assets/narration-DtEbQgCw.js +0 -1
  477. package/client/dist/assets/nav-ApJVg3xu.js +0 -1
  478. package/client/dist/assets/nav-BKNky1Bg.js +0 -1
  479. package/client/dist/assets/nav-C6vWWKYx.js +0 -1
  480. package/client/dist/assets/nav-CXJ6Aw89.js +0 -1
  481. package/client/dist/assets/nav-CsmFBIDf.js +0 -1
  482. package/client/dist/assets/nav-DjkmQJjd.js +0 -1
  483. package/client/dist/assets/packet-B1IxaL5f.js +0 -1
  484. package/client/dist/assets/packet-Bh5kkbiB.js +0 -1
  485. package/client/dist/assets/packet-By8fqYxW.js +0 -1
  486. package/client/dist/assets/packet-CZdvA44E.js +0 -1
  487. package/client/dist/assets/packet-CrjHRQCP.js +0 -1
  488. package/client/dist/assets/packet-Dwi2pTtx.js +0 -1
  489. package/client/dist/assets/packet-_-ZIa0IR.js +0 -1
  490. package/client/dist/assets/packet-n0W6E1Hn.js +0 -1
  491. package/client/dist/assets/play-DNlrawbS.js +0 -1
  492. package/client/dist/assets/provider-capabilities-Doej9lfa.js +0 -1
  493. package/client/dist/assets/settings-Bd0x4TBa.js +0 -1
  494. package/client/dist/assets/settings-C1-7cXzC.js +0 -1
  495. package/client/dist/assets/settings-CAFaT38b.js +0 -1
  496. package/client/dist/assets/settings-Ct3v1nW2.js +0 -1
  497. package/client/dist/assets/settings-DHh7_IF5.js +0 -1
  498. package/client/dist/assets/settings-DQXAmdcv.js +0 -1
  499. package/client/dist/assets/settings-guzlhpup.js +0 -1
  500. package/client/dist/assets/settings-sZfTPMEH.js +0 -1
  501. package/client/dist/assets/setup-3jWaoVUO.js +0 -1
  502. package/client/dist/assets/setup-BjgRneb2.js +0 -1
  503. package/client/dist/assets/setup-BzFIjv-f.js +0 -1
  504. package/client/dist/assets/setup-C4VfQsxE.js +0 -1
  505. package/client/dist/assets/setup-DfUkpDbN.js +0 -1
  506. package/client/dist/assets/setup-ECe90Pmu.js +0 -1
  507. package/client/dist/assets/setup-dlqC4Jmm.js +0 -1
  508. package/client/dist/assets/setup-jwfLMiM2.js +0 -1
  509. package/client/dist/assets/spending-EC9xqio-.js +0 -1
  510. package/client/dist/assets/useDesktop-BQVt8lFd.js +0 -1
@@ -14,6 +14,7 @@ const explore_cwd_manager_1 = require("./explore-cwd-manager");
14
14
  const ai_invocations_1 = require("./ai-invocations");
15
15
  const result_event_1 = require("./result-event");
16
16
  const crypto_1 = require("crypto");
17
+ const codex_spawn_1 = require("./plugins/codex-spawn");
17
18
  const spec_draft_parser_1 = require("./spec-draft-parser");
18
19
  const attachment_manager_1 = require("./attachment-manager");
19
20
  const providers_1 = require("./providers");
@@ -25,6 +26,7 @@ const explore_stdin_session_1 = require("./explore-stdin-session");
25
26
  const workspace_resolution_1 = require("./workspace-resolution");
26
27
  const workspace_manager_1 = require("./workspace-manager");
27
28
  const blueprint_render_1 = require("./blueprint-render");
29
+ const spec_contract_prompt_1 = require("./spec-contract-prompt");
28
30
  const explore_draft_title_1 = require("./explore-draft-title");
29
31
  const COMMAND_INSTRUCTION = 'When you want to suggest a SpecRails command for the user to execute, wrap it in a command block like this: ' +
30
32
  ':::command\n/specrails:implement #42\n::: ' +
@@ -133,6 +135,7 @@ exports.EXPLORE_MAX_CONCURRENCY = 5;
133
135
  exports.EXPLORE_QUEUE_TIMEOUT_MS = 30 * 1000;
134
136
  // ─── ChatManager ──────────────────────────────────────────────────────────────
135
137
  class ChatManager {
138
+ projectRepositories;
136
139
  _broadcast;
137
140
  _db;
138
141
  _activeProcesses;
@@ -190,7 +193,8 @@ class ChatManager {
190
193
  _adapter;
191
194
  _projectId;
192
195
  _projectSlug;
193
- constructor(broadcast, db, cwd, projectName, provider, projectId, projectSlug) {
196
+ constructor(broadcast, db, cwd, projectName, provider, projectId, projectSlug, projectRepositories) {
197
+ this.projectRepositories = projectRepositories;
194
198
  this._broadcast = broadcast;
195
199
  this._db = db;
196
200
  this._cwd = cwd;
@@ -713,26 +717,21 @@ class ChatManager {
713
717
  '- dependsOnIndex is optional. When present it must point strictly backward to an earlier item in this batch;',
714
718
  ' the first item always omits it. Never point to the same item or a later item.',
715
719
  '',
716
- '## Canonical description contract',
720
+ '## Canonical description contract (premium bar)',
717
721
  '',
718
- 'description is English markdown with exactly these five ## headings, once each and in this order:',
719
- '1. ## Problem Statement — the concrete user/system problem confirmed by the blueprint and current code.',
720
- '2. ## Proposed Solution — observable behavior and integration with verified existing components/contracts.',
721
- '3. ## Out of Scope — at least two bullets naming adjacent work deliberately deferred.',
722
- '4. ## Technical Considerations — at least two bullets naming only verified paths and identifiers, plus data/contracts,',
723
- ' compatibility, risks, failure handling, observability, migrations, and test strategy where relevant.',
724
- '5. ## Estimated Complexity — Low/Medium/High/Very High plus one sentence explaining the estimate.',
725
- 'Do NOT put an ## Acceptance Criteria heading in description. The app folds the separate criteria array into',
726
- 'the final ticket deterministically.',
722
+ ...(0, spec_contract_prompt_1.premiumSpecContract)('verified').split('\n'),
727
723
  '',
728
724
  '## Acceptance and self-audit',
729
725
  '',
730
- '- acceptanceCriteria contains 4-10 non-empty, independent, testable outcomes rather than implementation steps.',
726
+ '- Every spec MUST reach the premium depth above; a restated title is a defect. Add code-grounded reports',
727
+ ' of what exists today where it sharpens the Problem Statement or the Proposed Solution.',
728
+ '- acceptanceCriteria contains 6-10 non-empty, independent, testable outcomes rather than implementation steps.',
731
729
  '- Across the criteria, cover intended functional behavior, observable failure/edge cases, compatibility where',
732
730
  ' relevant, and the automated unit/integration/end-to-end tests that prove the change.',
733
- '- Before specsComplete: true, audit every item for all payload fields, exact heading names/order, non-empty',
734
- ' sections, English content, 4-10 criteria, valid priority, milestone plus domain labels, unique titles,',
735
- ' verified code references, and strictly backward dependencies. Repair failures before marking complete.',
731
+ '- Before specsComplete: true, audit every item for all payload fields, exact heading names/order, the ###',
732
+ ' sub-blocks, the depth floors, non-empty sections, English content, 6-10 criteria, valid priority, milestone',
733
+ ' plus domain labels, unique titles, verified code references, and strictly backward dependencies. Repair',
734
+ ' failures before marking complete.',
736
735
  ];
737
736
  if (execution?.relocated && execution.repoDir) {
738
737
  lines.push('', '## Repository location (relocated project)', '', 'The process cwd is a Specrails workspace containing configuration and artifacts, NOT the source repository.', `The real source repository is at this absolute path: ${execution.repoDir}`, 'The same repository is mounted from the workspace as ./project and exported in SPECRAILS_REPO_DIR.', 'Use the absolute path above or ./project for every source list/search/read. Read/Grep/Glob tools do not expand', 'shell-variable expressions, so never pass literal ${SPECRAILS_REPO_DIR} or ${SPECRAILS_REPO_DIR:-.} as a path.', 'When writing specs, cite verified paths relative to the real repository (for example src/...), never paths', 'under the workspace and never a misleading project/ or .specrails/ prefix.');
@@ -913,6 +912,12 @@ class ChatManager {
913
912
  : lightweight
914
913
  ? this._buildLightweightSystemPrompt(conversationScope)
915
914
  : this._buildSystemPrompt();
915
+ if (conversation.kind === 'explore') {
916
+ const repositories = this.projectRepositories?.() ?? [];
917
+ if (repositories.length > 1) {
918
+ systemPrompt += `\n\nThis logical project shares one backlog across these repositories: ${JSON.stringify(repositories.map(({ id, name, path, isPrimary, kind }) => ({ id, name, path, isPrimary, kind })))}. Use these exact IDs in the optional repositoryIds array of spec-draft blocks to scope a spec to one or several repositories. Missing repositoryIds defaults to primary. Preserve the user's selected scope, describe shared contracts for cross-repository specs, and never invent repository IDs or split a spec merely because it touches several repos.`;
919
+ }
920
+ }
916
921
  if (hasAttachments)
917
922
  systemPrompt = `${systemPrompt}\n\n${attachment_manager_1.USER_ATTACHMENT_SYSTEM_NOTE}`;
918
923
  const binary = adapter.binary;
@@ -935,6 +940,9 @@ class ChatManager {
935
940
  const scopeFlags = conversationScope && adapter.id === 'claude'
936
941
  ? (0, context_scope_1.toolFlagsForScope)(conversationScope).args
937
942
  : [];
943
+ if (this._cwd && conversation.kind !== 'milestone' && (!conversationScope || conversationScope.mcp)) {
944
+ scopeFlags.push(...(0, codex_spawn_1.buildCodexPluginArgs)({ providerId: adapter.id, stateRoot: this._specrailsRoot() ?? this._cwd, repositoryPath: this._cwd, legacyProviderId: this._adapter.id }));
945
+ }
938
946
  // Inject the user's OWN already-approved MCP servers when scope.userMcp is
939
947
  // on. Claude-only via `--mcp-config` (codex reads ~/.codex natively, so
940
948
  // buildUserMcpArgs returns []). Independent of the `mcp` toggle (project
@@ -0,0 +1,83 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs')
3
+ const path = require('node:path')
4
+ const { execFile } = require('node:child_process')
5
+
6
+ const MAX_ENTRIES = 100_000
7
+ const MAX_DEPTH = 24
8
+
9
+ function validateArchiveNames(listing, { platform = process.platform } = {}) {
10
+ const names = listing.split(/\r?\n/).filter(Boolean)
11
+ assert(names.length > 0 && names.length <= MAX_ENTRIES, 'Chromium archive is empty or exceeds its entry limit.')
12
+ for (const name of names) {
13
+ assert(!/^[\\/]|^[A-Za-z]:|[\x00-\x1f\x7f]/.test(name) && !name.includes('\\') && !name.split('/').includes('..'), `Unsafe Chromium archive path: ${name}`)
14
+ if (platform === 'win32') {
15
+ for (const component of name.split('/').filter(part => part !== '' && part !== '.')) {
16
+ assert(!/[:<>"|?*]|[ .]$/.test(component), `Unsafe Windows Chromium archive path: ${name}`)
17
+ const stem = component.split('.')[0].replace(/[ .]+$/g, '')
18
+ assert(!/^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9¹²³]|LPT[1-9¹²³])$/i.test(stem), `Reserved Windows Chromium archive path: ${name}`)
19
+ }
20
+ }
21
+ }
22
+ }
23
+
24
+ function validateWindowsArchiveTypes(listing) {
25
+ const entries = listing.split(/\r?\n/).filter(Boolean)
26
+ assert(entries.length > 0 && entries.length <= MAX_ENTRIES, 'Chromium archive is empty or exceeds its entry limit.')
27
+ for (const entry of entries) {
28
+ // bsdtar (the Windows system tar) resolves GNU/PAX extensions before listing
29
+ // the entry type. Only the fixed permission field matters, never translated
30
+ // dates/owners or a textual link target. Windows Chromium needs no links.
31
+ assert(/^[-d][rwxStTs-]{9}[+@.]?\s/.test(entry), 'Windows Chromium archives must contain only regular files and directories; links and special entries are not allowed.')
32
+ }
33
+ }
34
+
35
+ function runTar(binary, args, { timeout = 180_000, maxBytes = 12 * 1024 * 1024, env } = {}) {
36
+ return new Promise((resolve, reject) => {
37
+ execFile(binary, args, { timeout, maxBuffer: maxBytes, env, encoding: 'utf8', windowsHide: true }, (error, stdout) => {
38
+ if (error) reject(error)
39
+ else resolve(stdout)
40
+ })
41
+ })
42
+ }
43
+
44
+ /** Admission must finish before any extraction. Windows tar.exe does not
45
+ * reliably reject writes through an archive-created link, unlike POSIX tar. */
46
+ async function validateChromiumArchive(archivePath, { platform = process.platform, run = runTar } = {}) {
47
+ assert(fs.lstatSync(archivePath).isFile(), 'Chromium archive must be a regular file.')
48
+ const size = fs.statSync(archivePath).size
49
+ assert(size > 0 && size <= 3 * 1024 ** 3, 'Chromium archive is empty or exceeds 3 GiB.')
50
+ const tar = platform === 'win32' ? 'tar.exe' : '/usr/bin/tar'
51
+ const env = { ...process.env, LC_ALL: 'C', LANG: 'C' }
52
+ delete env.TAR_OPTIONS
53
+ validateArchiveNames(await run(tar, ['-tf', archivePath], { env }), { platform })
54
+ if (platform === 'win32') validateWindowsArchiveTypes(await run(tar, ['-tvf', archivePath], { env }))
55
+ }
56
+
57
+ function inside(root, candidate) {
58
+ const relative = path.relative(root, candidate)
59
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))
60
+ }
61
+
62
+ /** Validate without following directories through links. macOS versioned
63
+ * framework symlinks are allowed only when their resolved target stays inside. */
64
+ function validateChromiumTree(root) {
65
+ const realRoot = fs.realpathSync(root)
66
+ let count = 0
67
+ const walk = (directory, depth) => {
68
+ assert(depth <= MAX_DEPTH, 'Chromium tree exceeds its depth limit.')
69
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
70
+ assert(++count <= MAX_ENTRIES, 'Chromium tree exceeds its entry limit.')
71
+ const file = path.join(directory, entry.name)
72
+ if (entry.isSymbolicLink()) assert(inside(realRoot, fs.realpathSync(file)), `Chromium link escapes the extracted tree: ${file}`)
73
+ else if (entry.isDirectory()) walk(file, depth + 1)
74
+ else assert(entry.isFile(), `Unsupported Chromium entry: ${file}`)
75
+ }
76
+ }
77
+ walk(realRoot, 0)
78
+ }
79
+
80
+ exports.validateArchiveNames = validateArchiveNames
81
+ exports.validateWindowsArchiveTypes = validateWindowsArchiveTypes
82
+ exports.validateChromiumArchive = validateChromiumArchive
83
+ exports.validateChromiumTree = validateChromiumTree
@@ -12,40 +12,19 @@ const path_1 = __importDefault(require("path"));
12
12
  const child_process_1 = require("child_process");
13
13
  const stream_1 = require("stream");
14
14
  const promises_1 = require("stream/promises");
15
+ const chromium_archive_cjs_1 = require("./chromium-archive.cjs");
15
16
  /**
16
- * Resolve the Chromium executable the browser-capture feature should launch.
17
+ * Discover bundled Chromium, extracting its distribution archive when necessary.
17
18
  *
18
- * In desktop mode (`SPECRAILS_IS_DESKTOP=1`) we ship Chromium INSIDE the app, but
19
- * NOT as an unpacked directory: Tauri's resource bundler dereferences symlinks when
20
- * it copies `bundle.resources` into the .app (tauri-apps/tauri#13219), which mangles
21
- * Chromium's versioned `*.framework` (the `Versions/Current` + top-level symlinks
22
- * become flat duplicate copies) and invalidates its code signature — macOS
23
- * notarization then rejects the app ("The signature of the binary is invalid").
19
+ * A transparent chromium.tar.gz preserves the versioned framework symlinks that
20
+ * Tauri resource copying would otherwise dereference. Release assembly signs and
21
+ * notarizes Chromium before packing it; extraction preserves that signed layout.
22
+ * The writable cache is an installation location, not a signing bypass.
24
23
  *
25
- * So we ship Chromium as a single OPAQUE, OBFUSCATED blob
26
- * (`<runtimes>/chromium/chromium.pak`):
27
- * - It is an XOR-transformed `chromium.tar.gz`. The notarization service recursively
28
- * unpacks archives it recognises (.zip → .tar.gz → .tar → …) and validates every
29
- * Mach-O inside; Chromium's ~50 nested binaries are only ad-hoc ("linker") signed
30
- * by Google, so a plain archive fails notarization. XOR-ing breaks the gzip/tar
31
- * magic, so the notary cannot identify the file as a container and treats it as
32
- * opaque data — nothing inside is inspected, and the app notarizes with NO
33
- * Developer-ID signing of Chromium. (Obfuscation, not security: the key is public.)
34
- * - Tauri also copies it as one regular file — nothing to dereference.
35
- * - At first use we reverse the XOR and extract it to a writable cache
36
- * (`~/.specrails/runtimes/chromium`), restoring the framework symlinks intact. The
37
- * extracted Chromium keeps Google's ad-hoc signature, which is sufficient to execute
38
- * on Apple Silicon, and — being self-extracted rather than downloaded — carries no
39
- * `com.apple.quarantine` xattr, so Gatekeeper does not block it.
40
- *
41
- * A plain `chromium.tar.gz`/`chromium.tar` is also accepted (e.g. unobfuscated local
42
- * builds). When no archive is present (dev, a runtimes-less build, a partial
43
- * extraction) we fall back to discovering an UNPACKED `<runtimes>/chromium` tree, and
44
- * finally to `null` so Playwright uses its own managed browser — never dead-ending.
45
- *
46
- * We DISCOVER rather than hard-code the executable path because Playwright's layout
47
- * changes across versions (`chrome-mac/Chromium.app` → `chrome-mac-arm64/Google Chrome
48
- * for Testing.app`, `chrome-win/chrome.exe`, `chrome-linux/chrome`).
24
+ * Older bundles used an XOR-encoded chromium.pak. Decode that format only for
25
+ * compatibility, preferring transparent archives whenever both are present.
26
+ * Unpacked development bundles and Playwright-managed browsers remain fallbacks.
27
+ * The executable is discovered because Playwright's platform layout can change.
49
28
  */
50
29
  const MAX_DEPTH = 6;
51
30
  function isFile(p) {
@@ -155,12 +134,9 @@ function resolveBundledChromiumPath() {
155
134
  }
156
135
  }
157
136
  /** Candidate archive names under `<runtimes>/chromium`, in preference order. */
158
- const ARCHIVE_NAMES = ['chromium.pak', 'chromium.tar.gz', 'chromium.tar'];
159
- // XOR key for the obfuscated `chromium.pak` blob. Keep byte-identical to KEY in
160
- // scripts/obfuscate-chromium.mjs — the round-trip is covered by a unit test.
161
- // (Safe to have changed at the rebrand: the .pak and this binary always ship
162
- // together in the same bundle, and the extraction cache re-extracts on a new
163
- // archive via the `.source` marker.)
137
+ const ARCHIVE_NAMES = ['chromium.tar.gz', 'chromium.tar', 'chromium.pak'];
138
+ // Legacy archive decoding only. The bytes must remain compatible with previously
139
+ // shipped chromium.pak files; new release assembly produces transparent archives.
164
140
  const OBFUSCATION_KEY = Buffer.from('specrails-desktop-chromium-pack-v1', 'utf8');
165
141
  /** Streaming XOR transform (symmetric: packs and unpacks). */
166
142
  function xorStream() {
@@ -185,10 +161,10 @@ function chromiumCacheDir() {
185
161
  return (process.env.SPECRAILS_CHROMIUM_CACHE_DIR ||
186
162
  path_1.default.join(os_1.default.homedir(), '.specrails', 'runtimes', 'chromium'));
187
163
  }
188
- /** Identity string for an archive (size:mtime) used to skip re-extraction. */
164
+ /** Bind the extraction to the selected archive and its precise filesystem revision. */
189
165
  function archiveIdentity(archivePath) {
190
166
  const st = fs_1.default.statSync(archivePath);
191
- return `${st.size}:${Math.round(st.mtimeMs)}`;
167
+ return JSON.stringify({ version: 2, path: fs_1.default.realpathSync(archivePath), size: st.size, mtimeMs: st.mtimeMs, ctimeMs: st.ctimeMs, dev: st.dev, ino: st.ino });
192
168
  }
193
169
  /** Resolve the platform `tar` binary. macOS/Linux ship `/usr/bin/tar`; Windows 10+ ships `tar` (bsdtar) on PATH. */
194
170
  function tarBinary() {
@@ -196,12 +172,18 @@ function tarBinary() {
196
172
  return '/usr/bin/tar';
197
173
  return 'tar';
198
174
  }
199
- /** Extract `archivePath` into `destDir` using the system tar (auto-detects gzip). */
175
+ /** Extract a preflight-validated archive using the system tar (auto-detects gzip). */
200
176
  function runTarExtract(archivePath, destDir) {
201
177
  return new Promise((resolve, reject) => {
202
- const child = (0, child_process_1.spawn)(tarBinary(), ['-xf', archivePath, '-C', destDir], { stdio: ['ignore', 'ignore', 'pipe'] });
178
+ // Apple tar must restore the metadata carrying the stapled ticket. These
179
+ // caller overrides are appropriate for source copies, not signed bundles.
180
+ const env = { ...process.env };
181
+ delete env.TAR_OPTIONS;
182
+ delete env.COPYFILE_DISABLE;
183
+ delete env.COPY_EXTENDED_ATTRIBUTES_DISABLE;
184
+ const child = (0, child_process_1.spawn)(tarBinary(), ['-xf', archivePath, '-C', destDir], { stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true, env });
203
185
  let stderr = '';
204
- child.stderr?.on('data', (d) => { stderr += d.toString(); });
186
+ child.stderr?.on('data', (d) => { stderr = (stderr + d.toString()).slice(-4096); });
205
187
  child.on('error', reject);
206
188
  child.on('close', (code) => {
207
189
  if (code === 0)
@@ -211,9 +193,9 @@ function runTarExtract(archivePath, destDir) {
211
193
  });
212
194
  });
213
195
  }
214
- // In-process single-slot cache so concurrent / StrictMode double-invokes share one
215
- // extraction instead of racing. Keyed by archive identity; invalidated if it changes.
216
- let _extractInflight = null;
196
+ // Share pending work and serialize replacement of the same cache directory.
197
+ // Completed promises are removed so deleted or changed cache files are rechecked.
198
+ const extractInflight = new Map();
217
199
  /** First archive that exists under `<runtimes>/chromium`, or null. */
218
200
  function findBundledArchive(runtimesPath) {
219
201
  for (const name of ARCHIVE_NAMES) {
@@ -223,8 +205,7 @@ function findBundledArchive(runtimesPath) {
223
205
  }
224
206
  return null;
225
207
  }
226
- async function extractAndDiscover(archivePath, identity) {
227
- const destRoot = chromiumCacheDir();
208
+ async function extractAndDiscover(archivePath, identity, destRoot) {
228
209
  const marker = path_1.default.join(destRoot, '.source');
229
210
  // Fast path: already extracted from this exact archive.
230
211
  try {
@@ -235,10 +216,10 @@ async function extractAndDiscover(archivePath, identity) {
235
216
  }
236
217
  }
237
218
  catch { /* not yet extracted / stale → fall through */ }
238
- // Extract to a temp sibling, then atomically swap in.
239
- const tmpDir = `${destRoot}.tmp-${process.pid}`;
240
- fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
241
- fs_1.default.mkdirSync(tmpDir, { recursive: true });
219
+ // Validate a complete extraction before publishing; retain the prior cache if
220
+ // publication fails (for example, while a running browser locks it on Windows).
221
+ fs_1.default.mkdirSync(path_1.default.dirname(destRoot), { recursive: true });
222
+ const tmpDir = fs_1.default.mkdtempSync(`${destRoot}.tmp-`);
242
223
  // An obfuscated `.pak` is XOR-decoded to a real `.tar.gz` first; plain archives
243
224
  // are fed straight to tar.
244
225
  const isPak = archivePath.endsWith('.pak');
@@ -249,7 +230,11 @@ async function extractAndDiscover(archivePath, identity) {
249
230
  await deobfuscate(archivePath, decodedTar);
250
231
  tarSource = decodedTar;
251
232
  }
233
+ // Windows tar can follow an archive-created link before a post-extraction
234
+ // check runs. Validate the complete archive before allowing any writes.
235
+ await (0, chromium_archive_cjs_1.validateChromiumArchive)(tarSource);
252
236
  await runTarExtract(tarSource, tmpDir);
237
+ (0, chromium_archive_cjs_1.validateChromiumTree)(tmpDir);
253
238
  const exeInTmp = discoverChromiumExecutable(tmpDir);
254
239
  if (!exeInTmp)
255
240
  throw new Error('no chromium executable found after extraction');
@@ -257,10 +242,29 @@ async function extractAndDiscover(archivePath, identity) {
257
242
  fs_1.default.chmodSync(exeInTmp, 0o755);
258
243
  }
259
244
  catch { /* perms best-effort */ }
260
- fs_1.default.rmSync(destRoot, { recursive: true, force: true });
261
- fs_1.default.mkdirSync(path_1.default.dirname(destRoot), { recursive: true });
262
- fs_1.default.renameSync(tmpDir, destRoot);
263
- fs_1.default.writeFileSync(marker, identity);
245
+ if (archiveIdentity(archivePath) !== identity)
246
+ throw new Error('Chromium archive changed during extraction; retry with the current bundle');
247
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.source'), identity);
248
+ const previous = `${tmpDir}.previous`;
249
+ let movedPrevious = false;
250
+ if (fs_1.default.existsSync(destRoot)) {
251
+ fs_1.default.renameSync(destRoot, previous);
252
+ movedPrevious = true;
253
+ }
254
+ try {
255
+ fs_1.default.renameSync(tmpDir, destRoot);
256
+ }
257
+ catch (error) {
258
+ if (movedPrevious)
259
+ fs_1.default.renameSync(previous, destRoot);
260
+ throw error;
261
+ }
262
+ if (movedPrevious) {
263
+ try {
264
+ fs_1.default.rmSync(previous, { recursive: true, force: true });
265
+ }
266
+ catch { /* a locked prior version can be cleaned up later */ }
267
+ }
264
268
  return discoverChromiumExecutable(destRoot);
265
269
  }
266
270
  finally {
@@ -272,8 +276,8 @@ async function extractAndDiscover(archivePath, identity) {
272
276
  /**
273
277
  * Resolve the bundled Chromium executable for the launch path, extracting the
274
278
  * shipped archive on first use. Returns `null` (never throws) when not in desktop
275
- * mode, when no bundle is present, or when extraction fails — so Playwright falls
276
- * back to its managed browser rather than dead-ending.
279
+ * mode, when no bundle is present, or when extraction fails. The caller may then
280
+ * try an installed Playwright-managed browser; this resolver does not download one.
277
281
  */
278
282
  async function resolveBundledChromiumExecutable() {
279
283
  if (process.env.SPECRAILS_IS_DESKTOP !== '1')
@@ -298,14 +302,18 @@ async function resolveBundledChromiumExecutable() {
298
302
  catch {
299
303
  return resolveBundledChromiumPath();
300
304
  }
301
- if (_extractInflight && _extractInflight.identity === identity) {
302
- return _extractInflight.promise;
303
- }
304
- const promise = extractAndDiscover(archivePath, identity).catch((err) => {
305
+ const destRoot = path_1.default.resolve(chromiumCacheDir());
306
+ const pending = extractInflight.get(destRoot);
307
+ if (pending?.identity === identity)
308
+ return pending.promise;
309
+ const extract = () => extractAndDiscover(archivePath, identity, destRoot);
310
+ const promise = (pending ? pending.promise.then(extract) : extract()).catch((err) => {
305
311
  console.error('[chromium-resolver] extraction failed:', err instanceof Error ? err.message : err);
306
- _extractInflight = null; // allow a later retry
307
- return resolveBundledChromiumPath();
312
+ return discoverChromiumExecutable(path_1.default.join(runtimesPath, 'chromium'));
313
+ }).finally(() => {
314
+ if (extractInflight.get(destRoot)?.promise === promise)
315
+ extractInflight.delete(destRoot);
308
316
  });
309
- _extractInflight = { identity, promise };
317
+ extractInflight.set(destRoot, { identity, promise });
310
318
  return promise;
311
319
  }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CodeActivityQueryError = void 0;
4
+ exports.parseCodeActivityQuery = parseCodeActivityQuery;
5
+ exports.listCodeActivity = listCodeActivity;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const project_repositories_1 = require("./project-repositories");
8
+ class CodeActivityQueryError extends Error {
9
+ status;
10
+ constructor(message, status = 400) {
11
+ super(message);
12
+ this.status = status;
13
+ }
14
+ }
15
+ exports.CodeActivityQueryError = CodeActivityQueryError;
16
+ const MAX_SCAN_ROWS = 2_000;
17
+ const MAX_SCAN_MS = 2_000;
18
+ const BATCH_SIZE = 200;
19
+ function scalar(value, field) {
20
+ if (value === undefined)
21
+ return undefined;
22
+ if (typeof value !== 'string' || !value.trim() || value.length > 2048)
23
+ throw new CodeActivityQueryError(`invalid_activity_${field}`);
24
+ return value;
25
+ }
26
+ function parseCodeActivityQuery(query, scopedRepositoryId) {
27
+ const repositoryId = scalar(query.repositoryId, 'repository');
28
+ if (scopedRepositoryId && repositoryId && repositoryId !== scopedRepositoryId)
29
+ throw new CodeActivityQueryError('repository_scope_mismatch');
30
+ const ticket = scalar(query.ticketId, 'ticket');
31
+ const limit = scalar(query.limit, 'limit');
32
+ if (ticket !== undefined && (!/^\d+$/.test(ticket) || !Number.isSafeInteger(Number(ticket)) || Number(ticket) <= 0))
33
+ throw new CodeActivityQueryError('invalid_activity_ticket');
34
+ if (limit !== undefined && (!/^\d+$/.test(limit) || Number(limit) < 1 || Number(limit) > 100))
35
+ throw new CodeActivityQueryError('invalid_activity_limit');
36
+ return {
37
+ repositoryId: scopedRepositoryId ?? repositoryId,
38
+ ticketId: ticket === undefined ? undefined : Number(ticket),
39
+ jobId: scalar(query.jobId, 'job'), limit: limit === undefined ? undefined : Number(limit),
40
+ cursor: scalar(query.cursor, 'cursor'),
41
+ };
42
+ }
43
+ /** Keyset pagination is fenced to the first request's maximum provenance ID.
44
+ * Newly captured events (including backdated ones) cannot move that snapshot.
45
+ * The cursor is a position, never authorization: every page rechecks membership,
46
+ * filters and current path policy. No provider or current source read is needed. */
47
+ async function listCodeActivity(db, project, query, filterPaths) {
48
+ const started = Date.now();
49
+ const limit = query.limit ?? 50;
50
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
51
+ throw new CodeActivityQueryError('invalid_activity_limit');
52
+ const members = (0, project_repositories_1.getProjectRepositories)(project);
53
+ const repositories = query.repositoryId ? members.filter(repository => repository.id === query.repositoryId) : members;
54
+ if (!repositories.length)
55
+ throw new CodeActivityQueryError('repository_not_found', 404);
56
+ const primary = repositories.find(repository => repository.isPrimary);
57
+ const repositoryById = new Map(repositories.map(repository => [repository.id, repository]));
58
+ const scope = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify({
59
+ projectId: project.id,
60
+ repositories: repositories.map(({ id, path, isPrimary }) => ({ id, path, isPrimary })).sort((a, b) => a.id.localeCompare(b.id)),
61
+ ticketId: query.ticketId ?? null, jobId: query.jobId ?? null,
62
+ })).digest('hex');
63
+ let cursor;
64
+ if (query.cursor) {
65
+ try {
66
+ if (query.cursor.length > 2048)
67
+ throw new Error();
68
+ cursor = JSON.parse(Buffer.from(query.cursor, 'base64url').toString('utf8'));
69
+ if (cursor.v !== 1 || cursor.scope !== scope || !Number.isSafeInteger(cursor.snapshot) || cursor.snapshot < 0 ||
70
+ !Number.isSafeInteger(cursor.id) || cursor.id <= 0 || cursor.id > cursor.snapshot || !Number.isSafeInteger(cursor.at))
71
+ throw new Error();
72
+ }
73
+ catch {
74
+ throw new CodeActivityQueryError('invalid_activity_cursor');
75
+ }
76
+ }
77
+ const snapshot = cursor?.snapshot ?? db.prepare('SELECT COALESCE(MAX(id), 0) AS id FROM file_provenance').get().id;
78
+ const columns = db.prepare('PRAGMA table_info(file_provenance)').all();
79
+ const hasRepository = columns.some(column => column.name === 'repository_id');
80
+ const hasDiffs = !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='file_provenance_diffs'").get();
81
+ const conditions = ['p.id <= ?'];
82
+ const parameters = [snapshot];
83
+ if (hasRepository) {
84
+ conditions.push(`(p.repository_id IN (${repositories.map(() => '?').join(',')})${primary ? ' OR p.repository_id IS NULL' : ''})`);
85
+ parameters.push(...repositories.map(repository => repository.id));
86
+ }
87
+ else if (!primary)
88
+ conditions.push('0 = 1');
89
+ if (query.ticketId !== undefined) {
90
+ conditions.push('p.ticket_id = ?');
91
+ parameters.push(query.ticketId);
92
+ }
93
+ if (query.jobId !== undefined) {
94
+ conditions.push('p.job_id = ?');
95
+ parameters.push(query.jobId);
96
+ }
97
+ const where = conditions.join(' AND ');
98
+ const join = hasDiffs ? 'LEFT JOIN file_provenance_diffs d ON d.provenance_id = p.id' : '';
99
+ const projection = `p.id, ${hasRepository ? 'p.repository_id' : 'NULL AS repository_id'}, p.file_path, p.ticket_id, p.job_id, p.kind, p.at, ${hasDiffs ? '(d.provenance_id IS NOT NULL) AS has_patch, d.truncated AS patch_truncated' : '0 AS has_patch, 0 AS patch_truncated'}`;
100
+ let last = cursor ? { at: cursor.at, id: cursor.id } : undefined;
101
+ const entries = [];
102
+ const warnings = new Set();
103
+ let scanned = 0;
104
+ let exhausted = false;
105
+ while (entries.length < limit && scanned < MAX_SCAN_ROWS && Date.now() - started < MAX_SCAN_MS) {
106
+ const rows = db.prepare(`SELECT ${projection} FROM file_provenance p ${join} WHERE ${where}
107
+ ${last ? 'AND (p.at < ? OR (p.at = ? AND p.id < ?))' : ''}
108
+ ORDER BY p.at DESC, p.id DESC LIMIT ?`).all(...parameters, ...(last ? [last.at, last.at, last.id] : []), Math.min(BATCH_SIZE, MAX_SCAN_ROWS - scanned));
109
+ if (!rows.length) {
110
+ exhausted = true;
111
+ break;
112
+ }
113
+ const allowedByRepository = new Map();
114
+ for (const repository of repositories) {
115
+ const paths = [...new Set(rows.filter(row => row.repository_id === repository.id || (row.repository_id == null && repository.isPrimary)).map(row => row.file_path))];
116
+ if (!paths.length)
117
+ continue;
118
+ const filtered = await filterPaths(repository, paths, Math.max(0, MAX_SCAN_MS - (Date.now() - started)));
119
+ allowedByRepository.set(repository.id, filtered.allowed);
120
+ if (filtered.unavailable)
121
+ warnings.add(`repository-unavailable:${repository.id}`);
122
+ else if (filtered.incomplete)
123
+ warnings.add(`ignore-unverified:${repository.id}`);
124
+ }
125
+ for (const row of rows) {
126
+ last = { at: row.at, id: row.id };
127
+ scanned++;
128
+ const repository = row.repository_id == null ? primary : repositoryById.get(row.repository_id);
129
+ if (repository && allowedByRepository.get(repository.id)?.has(row.file_path)) {
130
+ entries.push({ id: row.id, repositoryId: repository.id, repositoryName: repository.name,
131
+ path: row.file_path, jobId: row.job_id, ticketId: row.ticket_id, kind: row.kind, at: row.at,
132
+ hasPatch: !!row.has_patch, patchTruncated: !!row.patch_truncated });
133
+ }
134
+ if (entries.length >= limit)
135
+ break;
136
+ }
137
+ }
138
+ const more = !exhausted && last !== undefined && !!db.prepare(`SELECT 1 FROM file_provenance p WHERE ${where} AND (p.at < ? OR (p.at = ? AND p.id < ?)) LIMIT 1`).get(...parameters, last.at, last.at, last.id);
139
+ if (more && entries.length < limit)
140
+ warnings.add(scanned >= MAX_SCAN_ROWS ? 'scan-limit' : 'time-limit');
141
+ return {
142
+ entries, nextCursor: more && last ? Buffer.from(JSON.stringify({ v: 1, scope, snapshot, ...last })).toString('base64url') : null,
143
+ truncated: warnings.size > 0, warnings: [...warnings],
144
+ };
145
+ }