@vantran-se/openchamber-web 2.1.1 → 2.2.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 (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
@@ -313,6 +313,29 @@ describe('Git network operation planner', () => {
313
313
  expect(JSON.stringify(plans.publicPlan)).not.toContain('credential_one');
314
314
  });
315
315
 
316
+ it('plans clone and checkout hydration transfers to ssh:// endpoints that name their user', async () => {
317
+ const endpoint = 'ssh://git@modules.example/library.git';
318
+ const publicEndpoint = { displayUrl: 'ssh://modules.example/library.git', fingerprint: fingerprintRemoteUrl(endpoint) };
319
+ const clone = await makePlanner().planNetworkOperation({
320
+ operation: 'clone', remoteUrl: endpoint, destinationPath: '/new/repo', transportMode: 'system', unverifiedConfirmed: true,
321
+ });
322
+ expect(clone.internalPlan.rawEndpoint).toBe(endpoint);
323
+ expect(clone.publicPlan.target.remote).toEqual(publicEndpoint);
324
+
325
+ const plans = await makePlanner({ inspectCheckoutHydration: async () => ({
326
+ headSha: SHA,
327
+ requirements: [{ kind: 'submodule', path: 'vendor/library', endpoint: publicEndpoint }],
328
+ transfers: [{ kind: 'submodule', path: 'vendor/library', endpoint: publicEndpoint, rawEndpoint: endpoint }],
329
+ }) }).planNetworkOperation({
330
+ operation: 'checkout-hydration', directory: '/repository', repositoryId: 'repo_one',
331
+ bindingRevision: 4, configRevision: 'config_one', remote: existingInput('fetch').remote,
332
+ });
333
+ expect(plans.internalPlan.plannedTransfers).toEqual([{
334
+ kind: 'submodule', path: 'vendor/library', endpoint: publicEndpoint, rawEndpoint: endpoint,
335
+ }]);
336
+ expect(JSON.stringify(plans.publicPlan)).not.toContain('git@');
337
+ });
338
+
316
339
  it('rejects stale checkout hydration source before inspection and rejects absolute discovered paths', async () => {
317
340
  const inspectCheckoutHydration = vi.fn(async () => ({ headSha: SHA, requirements: [], transfers: [] }));
318
341
  const input = {
@@ -479,7 +502,9 @@ describe('Git network operation planner', () => {
479
502
  'https://user@example.com/repo.git',
480
503
  'https://example.com/repo.git?access_token=secret',
481
504
  'https://example.com/repo.git#private',
482
- 'ssh://git@example.com/owner/repo.git',
505
+ 'ssh://git:secret@example.com/owner/repo.git',
506
+ 'ssh://:secret@example.com/owner/repo.git',
507
+ 'ssh://-oProxyCommand=touch@example.com/owner/repo.git',
483
508
  'ssh://example.com/owner/repo.git?key=secret',
484
509
  'git@example.com:owner/repo.git?key=secret',
485
510
  'git@example.com:owner/repo.git#private',
@@ -506,6 +531,46 @@ describe('Git network operation planner', () => {
506
531
  await expect(planner.planNetworkOperation(input)).resolves.toHaveProperty('publicPlan.target.remote.endpoint.displayUrl', endpoint);
507
532
  });
508
533
 
534
+ it('plans every existing-remote operation for an ssh:// endpoint that names its user, keeping the user out of public data', async () => {
535
+ const endpoint = 'ssh://git@example.com/owner/repo.git';
536
+ const displayUrl = 'ssh://example.com/owner/repo.git';
537
+ const endpointAuthority = { ...authority, endpoint, endpointFingerprint: fingerprintRemoteUrl(endpoint) };
538
+ expect(endpointAuthority.endpointFingerprint).toBe(fingerprintRemoteUrl(displayUrl));
539
+ const remote = (name) => ({ name, endpoint: { displayUrl, fingerprint: endpointAuthority.endpointFingerprint } });
540
+ const deletion = existingInput('delete-remote-branch', { remote: remote('upstream') });
541
+ delete deletion.sourceRef;
542
+ const inputs = [
543
+ existingInput('fetch', { remote: remote('upstream') }),
544
+ existingInput('pull', { remote: remote('upstream') }),
545
+ existingInput('push', { remote: remote('upstream') }),
546
+ deletion,
547
+ syncInput({
548
+ fetch: { ...syncInput().fetch, remote: remote('upstream') },
549
+ push: { ...syncInput().push, remote: remote('origin') },
550
+ }),
551
+ ];
552
+ for (const input of inputs) {
553
+ const plans = await makePlanner({ validateGitTransportContext: async () => endpointAuthority }).planNetworkOperation(input);
554
+ expect(JSON.stringify(plans.publicPlan)).not.toContain('git@');
555
+ const rawEndpoints = input.operation === 'sync'
556
+ ? [plans.internalPlan.fetch.rawEndpoint, plans.internalPlan.push.rawEndpoint]
557
+ : [plans.internalPlan.rawEndpoint];
558
+ expect(rawEndpoints).toEqual(rawEndpoints.map(() => endpoint));
559
+ }
560
+ });
561
+
562
+ it('rejects a URL display endpoint that carries userinfo', async () => {
563
+ const endpoint = 'ssh://git@modules.example/child.git';
564
+ await expect(makePlanner().planNetworkOperation({
565
+ operation: 'clone', remoteUrl: authority.endpoint, destinationPath: '/new/repo',
566
+ transportMode: 'system', unverifiedConfirmed: true,
567
+ auxiliaryGrants: [{
568
+ kind: 'submodule', endpoint: { displayUrl: endpoint, fingerprint: fingerprintRemoteUrl(endpoint) },
569
+ transportMode: 'system', unverifiedConfirmed: true,
570
+ }],
571
+ })).rejects.toThrow('Git endpoint is invalid');
572
+ });
573
+
509
574
  it.each([
510
575
  'http://example.com/repo.git',
511
576
  'file:///tmp/repo',
@@ -514,6 +579,8 @@ describe('Git network operation planner', () => {
514
579
  'ext::command repo',
515
580
  'custom://example.com/repo',
516
581
  'https://user:secret@example.com/repo.git',
582
+ 'https://user@example.com/repo.git',
583
+ 'ssh://git:secret@example.com/owner/repo.git',
517
584
  'https://example.com/repo.git?token=secret',
518
585
  'git@example.com:../repo',
519
586
  ])('rejects unsafe clone endpoint %s', async (remoteUrl) => {
@@ -590,6 +657,41 @@ describe('Git network operation planner', () => {
590
657
  .rejects.toMatchObject({ code: 'GIT_NETWORK_OPERATION_AUTHORITY_CHANGED', status: 409 });
591
658
  });
592
659
 
660
+ // A sha256/base64url digest can legitimately start with '-' (~1 in 64). These
661
+ // are opaque values the server generated and the client echoes back, never CLI
662
+ // arguments, so the leading-dash injection guard must not reject them.
663
+ const DASH_DIGEST = '-GtzS0gxC9C70rmAQhz4wNDSF-h_ePs0Ztxk1GUtHns';
664
+
665
+ it('accepts a configRevision that begins with a dash', async () => {
666
+ const plans = await makePlanner().planNetworkOperation(existingInput('fetch', {
667
+ configRevision: DASH_DIGEST,
668
+ }));
669
+ expect(plans.publicPlan.target.configRevision).toBe(DASH_DIGEST);
670
+ });
671
+
672
+ it('accepts endpoint and transport digests that begin with a dash', async () => {
673
+ const endpoint = 'https://example.com/owner/repository.git';
674
+ const planner = makePlanner({
675
+ validateGitTransportContext: vi.fn(async () => ({
676
+ ...authority,
677
+ endpoint,
678
+ endpointFingerprint: DASH_DIGEST,
679
+ transportRevision: DASH_DIGEST,
680
+ })),
681
+ });
682
+ const plans = await planner.planNetworkOperation(existingInput('fetch', {
683
+ remote: { name: 'upstream', endpoint: { displayUrl: endpoint, fingerprint: DASH_DIGEST } },
684
+ }));
685
+ expect(plans.publicPlan.target.remote.endpoint.fingerprint).toBe(DASH_DIGEST);
686
+ expect(plans.internalPlan.transportRevision).toBe(DASH_DIGEST);
687
+ });
688
+
689
+ it('still rejects a digest carrying control characters', async () => {
690
+ await expect(makePlanner().planNetworkOperation(existingInput('fetch', {
691
+ configRevision: 'cfg\nbad',
692
+ }))).rejects.toMatchObject({ code: 'INVALID_GIT_NETWORK_OPERATION' });
693
+ });
694
+
593
695
  it('plans clone into a deterministic operation-owned sibling', async () => {
594
696
  const stat = vi.fn(async () => { const error = new Error('missing'); error.code = 'ENOENT'; throw error; });
595
697
  const plans = await makePlanner({ fsImpl: { stat } }).planNetworkOperation({
@@ -1,3 +1,5 @@
1
+ import { isPlainObject, isString } from '../shared/guards.js';
2
+
1
3
  const DEFAULT_MAX_ENTRIES = 256;
2
4
  const DEFAULT_TERMINAL_RETENTION_MS = 60 * 60 * 1000;
3
5
  const DEFAULT_PLANNED_RETENTION_MS = 15 * 60 * 1000;
@@ -11,11 +13,11 @@ const ERROR_CODES = new Set([
11
13
  'INVALID_REQUEST', 'NOT_FOUND', 'STALE_REPOSITORY', 'STALE_BINDING', 'STALE_CONFIG',
12
14
  'REMOTE_CHANGED', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
13
15
  'CONFLICT', 'CANCELLED', 'TIMEOUT', 'OUTCOME_UNKNOWN', 'RUNTIME_UNSUPPORTED', 'UNKNOWN',
14
- 'GIT_LFS_CLIENT_MISSING',
16
+ 'GIT_LFS_CLIENT_MISSING', 'CHECKOUT_TOO_LARGE',
15
17
  ]);
16
18
  const FAILURE_ERROR_CODES = new Set([
17
19
  'INVALID_REQUEST', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
18
- 'RUNTIME_UNSUPPORTED', 'GIT_LFS_CLIENT_MISSING', 'UNKNOWN',
20
+ 'RUNTIME_UNSUPPORTED', 'GIT_LFS_CLIENT_MISSING', 'CHECKOUT_TOO_LARGE', 'UNKNOWN',
19
21
  ]);
20
22
  const STATE_ERROR_CODES = Object.freeze({
21
23
  partial: FAILURE_ERROR_CODES,
@@ -74,10 +76,6 @@ const isValidHydration = (value) => isPlainObject(value)
74
76
  && value.lfs.length <= 256
75
77
  && value.lfs.every((entry) => isHydrationPart(entry, true))
76
78
  && expectedHydrationStatus([...value.submodules, ...value.lfs]) === value.status;
77
- const isPlainObject = (value) => value === Object(value)
78
- && !Array.isArray(value)
79
- && Object.getPrototypeOf(value) === Object.prototype;
80
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
81
79
  const isBoundedString = (value, max) => isString(value) && value.trim() === value
82
80
  && value.length > 0 && value.length <= max && !/[\0\r\n]/.test(value);
83
81
  const isSafeDisplayEndpoint = (value) => {
@@ -1,3 +1,4 @@
1
+ import { isPlainObject, isString } from '../shared/guards.js';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { randomUUID } from 'node:crypto';
@@ -20,11 +21,11 @@ const ERROR_CODES = new Set([
20
21
  'INVALID_REQUEST', 'NOT_FOUND', 'STALE_REPOSITORY', 'STALE_BINDING', 'STALE_CONFIG',
21
22
  'REMOTE_CHANGED', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
22
23
  'CONFLICT', 'CANCELLED', 'TIMEOUT', 'OUTCOME_UNKNOWN', 'RUNTIME_UNSUPPORTED', 'UNKNOWN',
23
- 'GIT_LFS_CLIENT_MISSING',
24
+ 'GIT_LFS_CLIENT_MISSING', 'CHECKOUT_TOO_LARGE',
24
25
  ]);
25
26
  const FAILURE_ERROR_CODES = new Set([
26
27
  'INVALID_REQUEST', 'AUTHENTICATION_REQUIRED', 'AUTHENTICATION_FAILED', 'TRANSPORT_FAILED',
27
- 'RUNTIME_UNSUPPORTED', 'GIT_LFS_CLIENT_MISSING', 'UNKNOWN',
28
+ 'RUNTIME_UNSUPPORTED', 'GIT_LFS_CLIENT_MISSING', 'CHECKOUT_TOO_LARGE', 'UNKNOWN',
28
29
  ]);
29
30
  const STATE_ERROR_CODES = Object.freeze({
30
31
  partial: FAILURE_ERROR_CODES,
@@ -54,12 +55,9 @@ const SAFE_ERROR_MESSAGES = Object.freeze({
54
55
  RUNTIME_UNSUPPORTED: 'Git operation is unsupported by this runtime',
55
56
  UNKNOWN: 'Git operation failed; inspect repository state before retrying',
56
57
  GIT_LFS_CLIENT_MISSING: 'Git LFS is required but unavailable',
58
+ CHECKOUT_TOO_LARGE: 'Checkout has too many files to inspect',
57
59
  });
58
60
 
59
- const isPlainObject = (value) => value === Object(value)
60
- && !Array.isArray(value)
61
- && Object.getPrototypeOf(value) === Object.prototype;
62
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
63
61
  const exactKeys = (value, required, optional = []) => {
64
62
  const keys = Object.keys(value);
65
63
  return required.every((key) => keys.includes(key))
@@ -1,3 +1,4 @@
1
+ import { isString } from '../shared/guards.js';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { spawn } from 'node:child_process';
@@ -41,7 +42,6 @@ const MANAGED_ENV_NAMES = new Set([
41
42
  'NO_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy', 'NETRC',
42
43
  ]);
43
44
  const SHA_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/i;
44
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
45
45
 
46
46
  const operationError = (code, message, status = 500, details = {}) => Object.assign(new Error(message), { code, status, ...details });
47
47
  const publicError = (code, message) => ({ code, message });
@@ -796,6 +796,12 @@ export function createNetworkOperations({
796
796
  if (error?.code === 'GIT_AUXILIARY_AUTHORIZATION_REQUIRED') {
797
797
  return { status: 'authorization-required', error: publicError('AUTHENTICATION_REQUIRED', 'This checkout endpoint requires an explicit credential grant') };
798
798
  }
799
+ // An LFS discovery bound trips on the number of tracked files. It gets its
800
+ // own code so the message names that; the configuration message sent
801
+ // people to fix settings that were fine.
802
+ if (error?.code === 'LFS_DISCOVERY_LIMIT_EXCEEDED') {
803
+ return { status: 'invalid', error: publicError('CHECKOUT_TOO_LARGE', 'Checkout has too many files to inspect') };
804
+ }
799
805
  if (String(error?.code || '').includes('INVALID') || String(error?.code || '').includes('LIMIT_EXCEEDED')
800
806
  || error?.code === 'UNSAFE_LFS_EXECUTABLE_CONFIG') {
801
807
  return { status: 'invalid', error: publicError('INVALID_REQUEST', 'Checkout hydration configuration is invalid') };
@@ -1979,8 +1985,9 @@ export function createNetworkOperations({
1979
1985
  await controls.markStepCompleted('updated-local-repository');
1980
1986
  if (['succeeded', 'not-needed'].includes(hydration.status)) result = { state: 'succeeded', hydration };
1981
1987
  else {
1982
- const unsupported = [...hydration.submodules, ...hydration.lfs].find((entry) => entry.error?.code === 'RUNTIME_UNSUPPORTED');
1983
- const failure = unsupported?.error ?? (hydration.status === 'client-missing'
1988
+ const specific = [...hydration.submodules, ...hydration.lfs]
1989
+ .find((entry) => ['RUNTIME_UNSUPPORTED', 'CHECKOUT_TOO_LARGE'].includes(entry.error?.code));
1990
+ const failure = specific?.error ?? (hydration.status === 'client-missing'
1984
1991
  ? publicError('GIT_LFS_CLIENT_MISSING', 'Git LFS is required for this checkout; install git-lfs and retry')
1985
1992
  : hydration.status === 'authorization-required'
1986
1993
  ? publicError('AUTHENTICATION_REQUIRED', 'Checkout hydration requires an explicit endpoint grant')
@@ -2099,7 +2106,8 @@ export function createNetworkOperations({
2099
2106
  return { state: 'failed', error: publicError('AUTHENTICATION_REQUIRED', 'Checkout hydration requires an explicit endpoint grant'), hydration };
2100
2107
  }
2101
2108
  if (hydration.status === 'invalid') {
2102
- return { state: 'failed', error: publicError('INVALID_REQUEST', 'Checkout hydration configuration is invalid'), hydration };
2109
+ const tooLarge = [...hydration.submodules, ...hydration.lfs].find((entry) => entry.error?.code === 'CHECKOUT_TOO_LARGE');
2110
+ return { state: 'failed', error: tooLarge?.error ?? publicError('INVALID_REQUEST', 'Checkout hydration configuration is invalid'), hydration };
2103
2111
  }
2104
2112
  const unsupported = [...hydration.submodules, ...hydration.lfs].find((entry) => entry.error?.code === 'RUNTIME_UNSUPPORTED');
2105
2113
  return { state: 'failed', error: unsupported?.error ?? publicError('TRANSPORT_FAILED', 'Checkout hydration failed'), hydration };
@@ -2336,17 +2336,27 @@ process.exit(safe ? 0 : 1);
2336
2336
  });
2337
2337
  });
2338
2338
 
2339
- it('returns the stable missing git-lfs result without starting an LFS transfer', async () => {
2339
+ // Past the old 256-record ceiling too: a repository of thousands of LFS
2340
+ // files reaches the same client check instead of a discovery failure.
2341
+ it.each([1, 3_000])('returns the stable missing git-lfs result for %i LFS files without starting an LFS transfer', async (count) => {
2340
2342
  const pointer = `version https://git-lfs.github.com/spec/v1\noid sha256:${'d'.repeat(64)}\nsize 12\n`;
2343
+ const files = Array.from({ length: count }, (_, index) => `assets/asset-${index}.bin`);
2344
+ const batch = (child, separator) => child.stdin.read().toString().slice(0, -1).split(separator);
2341
2345
  const setupValue = setup({
2342
2346
  spawnResponder: ({ args }) => {
2343
2347
  const command = args.join(' ');
2344
2348
  if (command.includes('config --blob HEAD:.gitmodules')) return { code: 1 };
2345
2349
  if (command.includes('ls-tree -rz')) return { code: 0 };
2346
- if (command.includes('ls-files -z')) return { code: 0, stdout: 'asset.bin\0' };
2347
- if (command.includes('check-attr')) return { code: 0, stdout: 'asset.bin\0filter\0lfs\0' };
2348
- if (command.includes('cat-file --batch-check')) return { code: 0, stdout: `${SHA} blob ${Buffer.byteLength(pointer)}\n` };
2349
- if (command.includes('cat-file --batch')) return { code: 0, stdout: `${SHA} blob ${Buffer.byteLength(pointer)}\n${pointer}\n` };
2350
+ if (command.includes('ls-files -z')) return { code: 0, stdout: `${files.join('\0')}\0` };
2351
+ if (command.includes('check-attr')) return { onSpawn: ({ child }) => {
2352
+ child.stdout.write(batch(child, '\0').map((file) => `${file}\0filter\0lfs\0`).join(''));
2353
+ } };
2354
+ if (command.includes('cat-file --batch-check')) return { onSpawn: ({ child }) => {
2355
+ child.stdout.write(batch(child, '\n').map(() => `${SHA} blob ${Buffer.byteLength(pointer)}\n`).join(''));
2356
+ } };
2357
+ if (command.includes('cat-file --batch')) return { onSpawn: ({ child }) => {
2358
+ child.stdout.write(batch(child, '\n').map(() => `${SHA} blob ${Buffer.byteLength(pointer)}\n${pointer}\n`).join(''));
2359
+ } };
2350
2360
  if (command.includes('HEAD:.lfsconfig') || command.includes('config --includes')) return { code: 1 };
2351
2361
  if (command.includes('lfs version')) return { code: 1, stderr: 'git-lfs is not installed at /private/bin' };
2352
2362
  return { code: 0 };
@@ -2502,7 +2512,10 @@ process.exit(safe ? 0 : 1);
2502
2512
  plan = await operation.service.plan({ operation: 'clone', remoteUrl: ENDPOINT, destinationPath: destination, transportMode: 'system', unverifiedConfirmed: true });
2503
2513
  const result = await operation.service.execute(plan.operationId);
2504
2514
  expect(result).toMatchObject(interruption === 'overflow'
2505
- ? { state: 'partial', hydration: { status: 'invalid' } }
2515
+ ? {
2516
+ state: 'partial', error: { code: 'CHECKOUT_TOO_LARGE' },
2517
+ hydration: { status: 'invalid', lfs: [{ status: 'invalid', error: { code: 'CHECKOUT_TOO_LARGE' } }] },
2518
+ }
2506
2519
  : { state: 'cancelled', error: { code: interruption.startsWith('CANCELLED') ? 'CANCELLED' : interruption }, hydration: { status: 'cancelled' } });
2507
2520
  expect(metadataBatches).toBe(2);
2508
2521
  expect(operation.calls.filter((call) => call.args.includes('cat-file'))).toHaveLength(4);
@@ -1,8 +1,9 @@
1
+ import { isString } from '../shared/guards.js';
2
+
1
3
  const DEFAULT_MAX_CHARS = 8_192;
2
4
  const REDACTED = '[redacted]';
3
5
  const SENSITIVE_QUERY_KEYS = /^(?:access_token|auth|authorization|credential|key|oauth_token|password|private_token|secret|token)$/i;
4
6
  const URL_PATTERN = /[a-z][a-z0-9+.-]*:\/\/[^\s<>"']+/gi;
5
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
6
7
 
7
8
  const stringValue = (value) => {
8
9
  if (value instanceof Error) {
@@ -1,3 +1,4 @@
1
+ import { isString } from '../shared/guards.js';
1
2
  import crypto from 'node:crypto';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
@@ -11,8 +12,6 @@ const MAX_QUERY_BYTES = 64 * 1024;
11
12
  const NONE = Object.freeze({ mode: 'none' });
12
13
  const SYSTEM = Object.freeze({ mode: 'system' });
13
14
 
14
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
15
-
16
15
  /** The origin a redacted remote URL points at, or null when it is not HTTPS. */
17
16
  const httpsOrigin = (displayUrl) => {
18
17
  try {
@@ -122,6 +121,7 @@ export function createGitRepositoryCredentialRuntime({
122
121
  getActivePort,
123
122
  getActiveHost = () => null,
124
123
  helperPath = HELPER_PATH,
124
+ helperLaunch,
125
125
  randomBytes = crypto.randomBytes,
126
126
  }) {
127
127
  if (!(readBinding instanceof Function) || !credentialResolver || !isString(dataDir)
@@ -205,7 +205,9 @@ export function createGitRepositoryCredentialRuntime({
205
205
  activeSecret = randomBytes(32).toString('base64url');
206
206
  const url = `http://${callbackHost()}:${port}/api/git/repository-credential`;
207
207
  await writePrivate(endpointFilePath, `${JSON.stringify({ version: 1, url, secret: activeSecret })}\n`, 0o600);
208
- const script = `#!/bin/sh\nexec ${helperShellCommand(path.resolve(helperPath), [endpointFilePath])} "$@"\n`;
208
+ // No `exec`: under Electron the command starts with a variable
209
+ // assignment, which `exec` would take for the program to run.
210
+ const script = `#!/bin/sh\n${helperShellCommand(path.resolve(helperPath), [endpointFilePath], helperLaunch)} "$@"\n`;
209
211
  await writePrivate(launcherPath, script, 0o700);
210
212
  },
211
213
  registerRoutes: (app) => {
@@ -64,6 +64,28 @@ describe('createGitRepositoryCredentialRuntime', () => {
64
64
  expect(runtime.helperCommand()).toBe(`!'${path.join(dataDir, 'bin', 'git-credential-openchamber')}'`);
65
65
  });
66
66
 
67
+ it.skipIf(process.platform === 'win32')('publishes a launcher that runs the helper as Node under Electron', async () => {
68
+ const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openchamber-repo-credential-'));
69
+ roots.push(dataDir);
70
+ const executable = path.join(dataDir, 'electron');
71
+ await fs.writeFile(executable, '#!/bin/sh\nprintf "%s|%s" "$ELECTRON_RUN_AS_NODE" "$*"\n', { mode: 0o700 });
72
+ const runtime = createGitRepositoryCredentialRuntime({
73
+ readBinding: async () => read(null),
74
+ credentialResolver: { resolve: async () => null },
75
+ dataDir, fsPromises: fs,
76
+ getActivePort: () => 4399,
77
+ helperPath: '/opt/openchamber/repository-credential-helper.js',
78
+ helperLaunch: { execPath: executable, versions: { electron: '1.0.0' } },
79
+ });
80
+ await runtime.publish();
81
+
82
+ const launcher = path.join(dataDir, 'bin', 'git-credential-openchamber');
83
+ const output = await new Promise((resolve, reject) => {
84
+ execFile(launcher, ['get'], (error, stdout) => (error ? reject(error) : resolve(stdout)));
85
+ });
86
+ expect(output).toBe(`1|/opt/openchamber/repository-credential-helper.js ${path.join(dataDir, 'git-credential-endpoint.json')} get`);
87
+ });
88
+
67
89
  it('answers the bound account credential for the repository the helper runs in', async () => {
68
90
  const { runtime, dataDir, resolved } = await setup();
69
91
  await runtime.publish();
@@ -1,14 +1,12 @@
1
+ import { isPlainObject, isString } from '../shared/guards.js';
1
2
  import { OpenCode } from '@opencode/client';
2
3
  import { createGitRedactor, redactGitText } from './redaction.js';
3
4
  import { runAsUserAction } from '../environment/refresh-scope.js';
4
5
  import { redactRemoteUrl } from '../source-control/url-redaction.js';
5
6
  import { parsePublicGitIdentityProfile, toPublicGitIdentityProfile } from './identity-storage.js';
7
+ import { WorktreeCreationError, createWorktreeCreation, hasClientContributorAuthority } from './worktree-creation.js';
6
8
 
7
9
  const NETWORK_OPERATION_ID = /^[A-Za-z0-9_-]{1,200}$/;
8
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
9
- const isPlainObject = (value) => value === Object(value)
10
- && !Array.isArray(value)
11
- && Object.getPrototypeOf(value) === Object.prototype;
12
10
  const toGitIdentitySummary = (identity) => identity ? {
13
11
  userName: identity.userName ?? null,
14
12
  userEmail: identity.userEmail ?? null,
@@ -180,35 +178,15 @@ export function registerGitRoutes(app, {
180
178
  return res.status(500).json({ code: 'UNKNOWN', error: errors[input.operation] });
181
179
  }
182
180
  });
183
- const hasClientContributorAuthority = (body) => body?.contributorFork !== undefined
184
- || body?.changeRequestTransfer !== undefined || body?.contributorTransferComplete !== undefined
185
- || (body?.changeRequestSource && body?.ensureRemoteUrl !== undefined);
186
- // A change request from this repository itself is fetched onto the primary
187
- // remote it already has, under that remote's own URL, so the checkout adds no
188
- // second remote for the same project. Only a fork gets a remote of its own.
189
- // A fork's remote is named after its owner, which the provider just said; the
190
- // picker cannot always know it (GitLab lists merge requests without it).
191
- const forkRemoteName = (owner) => `pr-${String(owner || '').trim().toLowerCase()
192
- .replace(/[^a-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'head'}`;
193
- const changeRequestSourceOnRepository = async (directory, source) => {
194
- if (source.classification !== 'same-repository') {
195
- return source.sourceProject?.owner
196
- ? Object.freeze({ ...source, requestedRemoteName: forkRemoteName(source.sourceProject.owner) })
197
- : source;
198
- }
199
- const { getRepositoryRemoteUrls } = await getGitLibraries();
200
- const primary = (await getRepositoryRemoteUrls(directory)).find((remote) => remote.name === source.context.primaryRemote);
201
- if (!primary?.fetchUrl) return source;
202
- return Object.freeze({ ...source, requestedRemoteName: primary.name, remoteUrl: primary.fetchUrl });
203
- };
204
- const worktreeInputForSource = (input, source) => ({
205
- ...input,
206
- changeRequestSource: undefined,
207
- contributorFork: source.classification === 'contributor-fork',
208
- changeRequestTransfer: true,
209
- ensureRemoteName: source.requestedRemoteName,
210
- ensureRemoteUrl: source.remoteUrl ?? source.endpoint,
211
- expectedRevision: source.headSha,
181
+ const worktreeCreation = createWorktreeCreation({
182
+ getGitLibraries,
183
+ networkOperations,
184
+ getSourceControlBinding,
185
+ contributorProvenance,
186
+ resolveChangeRequestSource,
187
+ createHttpsCredentialReference,
188
+ resolveSourceControlAccount,
189
+ worktreeBootstrapStore,
212
190
  });
213
191
  const rejectLegacyNetworkOperation = async (directory, res) => {
214
192
  if (contributorProvenance?.read instanceof Function) {
@@ -1290,7 +1268,6 @@ export function registerGitRoutes(app, {
1290
1268
  }
1291
1269
  });
1292
1270
 
1293
-
1294
1271
  app.put('/api/git/branches/rename', async (req, res) => {
1295
1272
  const { renameBranch } = await getGitLibraries();
1296
1273
  try {
@@ -1310,6 +1287,9 @@ export function registerGitRoutes(app, {
1310
1287
  const result = await renameBranch(directory, oldName, newName);
1311
1288
  res.json(result);
1312
1289
  } catch (error) {
1290
+ if (error.statusCode === 409) {
1291
+ return res.status(409).json({ error: error.message });
1292
+ }
1313
1293
  console.error('Failed to rename branch:', error);
1314
1294
  res.status(500).json({ error: error.message || 'Failed to rename branch' });
1315
1295
  }
@@ -1442,8 +1422,9 @@ export function registerGitRoutes(app, {
1442
1422
  }
1443
1423
  }
1444
1424
  }
1445
- // A repository always lists at least its primary worktree; an empty
1446
- // list means "not a repository" and has no topology to track.
1425
+ // An empty list means "not a repository", or a bare repository with no
1426
+ // linked worktree yet; neither has topology to track. A worktree created
1427
+ // from the terminal in such a repository shows up on the next list call.
1447
1428
  if (worktrees.length > 0) {
1448
1429
  void observeWorktreeTopology(directory);
1449
1430
  }
@@ -1475,123 +1456,33 @@ export function registerGitRoutes(app, {
1475
1456
  if (hasClientContributorAuthority(req.body)) {
1476
1457
  return res.status(400).json({ error: 'Invalid contributor worktree request', code: 'INVALID_CONTRIBUTOR_WORKTREE' });
1477
1458
  }
1478
- let input = req.body || {};
1479
- if (input.changeRequestSource) {
1480
- if (!(resolveChangeRequestSource instanceof Function)) return res.status(501).json({ error: 'Contributor worktrees are unavailable', code: 'RUNTIME_UNSUPPORTED' });
1481
- const source = await changeRequestSourceOnRepository(directory, await resolveChangeRequestSource(input.changeRequestSource));
1482
- input = worktreeInputForSource(input, source);
1483
- }
1459
+ const input = await worktreeCreation.validationInput(directory, req.body || {});
1484
1460
  const result = await validateWorktreeCreate(directory, input);
1485
1461
  res.json(result);
1486
1462
  } catch (error) {
1463
+ if (error instanceof WorktreeCreationError) {
1464
+ return res.status(error.status).json({ error: error.message, code: error.code });
1465
+ }
1487
1466
  console.error('Failed to validate worktree creation:', error);
1488
1467
  res.status(500).json({ error: error.message || 'Failed to validate worktree creation' });
1489
1468
  }
1490
1469
  });
1491
1470
 
1492
1471
  app.post('/api/git/worktrees', userAction(async (req, res) => {
1493
- const { createWorktree, validateWorktreeCreate } = await getGitLibraries();
1494
- if (typeof createWorktree !== 'function' || typeof validateWorktreeCreate !== 'function') {
1495
- return res.status(501).json({ error: 'Worktree creation is not available' });
1496
- }
1497
1472
  try {
1498
1473
  const directory = req.query.directory;
1499
1474
  if (!directory || typeof directory !== 'string') {
1500
1475
  return res.status(400).json({ error: 'directory parameter is required' });
1501
1476
  }
1502
-
1503
- if (hasClientContributorAuthority(req.body)) {
1504
- return res.status(400).json({ error: 'Invalid contributor worktree request', code: 'INVALID_CONTRIBUTOR_WORKTREE' });
1505
- }
1506
- if (!(networkOperations?.hydrateBoundCheckout instanceof Function)
1507
- || !(worktreeBootstrapStore?.read instanceof Function)
1508
- || !(worktreeBootstrapStore?.write instanceof Function)) {
1509
- return res.status(501).json({ error: 'Worktree checkout bootstrap is not available' });
1510
- }
1511
- let input = req.body || {};
1512
- let source = null;
1513
- if (input.changeRequestSource) {
1514
- if (!(resolveChangeRequestSource instanceof Function)
1515
- || !(createHttpsCredentialReference instanceof Function)
1516
- || !(resolveSourceControlAccount instanceof Function)
1517
- || !(networkOperations?.transferContributorHead instanceof Function)) {
1518
- return res.status(501).json({ error: 'Contributor worktrees are unavailable', code: 'RUNTIME_UNSUPPORTED' });
1519
- }
1520
- source = await changeRequestSourceOnRepository(directory, await resolveChangeRequestSource(input.changeRequestSource));
1521
- const headBranch = source.headRef.slice('refs/heads/'.length);
1522
- const destinationRef = `refs/remotes/${source.requestedRemoteName}/${headBranch}`;
1523
- input = {
1524
- ...worktreeInputForSource(input, source),
1525
- existingBranch: destinationRef.slice('refs/'.length),
1526
- setUpstream: false,
1527
- };
1528
- const validation = await validateWorktreeCreate(directory, input);
1529
- if (!validation.ok && !validation.errors.every((entry) => entry.code === 'contributor_transfer_unavailable')) {
1530
- // The same error shape as every other refusal here, so the reason reaches the user.
1531
- const collision = validation.errors.find((entry) => entry.code === 'remote_name_collision');
1532
- return res.status(409).json(collision
1533
- ? { error: collision.message, code: 'CONTRIBUTOR_REMOTE_COLLISION', remoteName: input.ensureRemoteName }
1534
- : {
1535
- error: validation.errors.map((entry) => entry.message).filter(Boolean).join('\n') || 'Failed to validate worktree creation',
1536
- code: 'INVALID_REQUEST',
1537
- });
1538
- }
1539
- const account = await resolveSourceControlAccount(source.context);
1540
- if (account?.credentialId !== source.context.accountId || account?.status !== 'valid'
1541
- || !Number.isSafeInteger(account?.credentialRevision) || account.credentialRevision < 1
1542
- || Object.prototype.toString.call(account?.providerUserId) !== '[object String]' || !account.providerUserId) {
1543
- throw Object.assign(new Error('Contributor credential is unavailable'), { code: 'AUTHENTICATION_REQUIRED', status: 409 });
1544
- }
1545
- const credentialId = createHttpsCredentialReference({
1546
- provider: source.context.provider,
1547
- instance: source.context.instance,
1548
- credentialId: account.credentialId,
1549
- credentialRevision: account.credentialRevision,
1550
- providerUserId: account.providerUserId,
1551
- });
1552
- const transfer = await networkOperations.transferContributorHead({
1553
- directory,
1554
- sourceRequest: req.body.changeRequestSource,
1555
- source,
1556
- destinationRef,
1557
- credentialId,
1558
- });
1559
- if (transfer.state !== 'succeeded') {
1560
- const status = transfer.error?.code === 'AUTHENTICATION_REQUIRED' ? 401 : 409;
1561
- return res.status(status).json({ error: transfer.error?.message || 'Contributor head transfer failed', code: transfer.error?.code || 'UNKNOWN' });
1562
- }
1563
- input.contributorTransferComplete = true;
1564
- }
1565
- const bindingRead = getSourceControlBinding instanceof Function
1566
- ? await getSourceControlBinding(directory)
1567
- : null;
1568
- const repositoryAuthority = bindingRead?.binding ? {
1569
- repositoryId: bindingRead.repository.repositoryId,
1570
- bindingRevision: bindingRead.revision,
1571
- configRevision: bindingRead.repository.configRevision,
1572
- } : null;
1573
- const hydrateCheckout = ({ directory: checkoutDirectory, parentRemoteName }) => networkOperations.hydrateBoundCheckout({
1574
- directory: checkoutDirectory,
1575
- parentRemoteName,
1576
- parentEndpoint: source?.endpoint,
1577
- repositoryAuthority,
1578
- });
1579
- const created = await createWorktree(directory, input, {
1580
- contributorProvenance,
1581
- contributorSource: source,
1582
- hydrateCheckout,
1583
- bootstrapStore: worktreeBootstrapStore,
1584
- });
1585
- res.json(created);
1477
+ res.json(await worktreeCreation.create(directory, req.body || {}));
1586
1478
  } catch (error) {
1587
- console.error('Failed to create worktree:', error);
1588
- if (error?.code === 'CONTRIBUTOR_REMOTE_COLLISION') {
1589
- return res.status(409).json({
1590
- error: 'Contributor remote name is already used by a different endpoint',
1591
- code: 'CONTRIBUTOR_REMOTE_COLLISION',
1592
- remoteName: error.remoteName,
1593
- });
1479
+ // A refusal carries the same error shape as every other one here, so the reason reaches the user.
1480
+ if (error instanceof WorktreeCreationError) {
1481
+ const body = { error: error.message, code: error.code };
1482
+ if (error.remoteName !== undefined) body.remoteName = error.remoteName;
1483
+ return res.status(error.status).json(body);
1594
1484
  }
1485
+ console.error('Failed to create worktree:', error);
1595
1486
  res.status(Number.isInteger(error?.status) ? error.status : 500).json({
1596
1487
  error: error.message || 'Failed to create worktree',
1597
1488
  code: error?.code,
@@ -1835,4 +1726,7 @@ export function registerGitRoutes(app, {
1835
1726
  }
1836
1727
  });
1837
1728
 
1729
+ // The session service creates worktrees from change requests through the
1730
+ // same checks the route uses.
1731
+ return { createWorktree: worktreeCreation.create };
1838
1732
  }
@@ -527,7 +527,7 @@ describe('contributor worktree authority', () => {
527
527
  expect(hydrateBoundCheckout).toHaveBeenCalledWith(expect.objectContaining({ parentRemoteName: '' }));
528
528
  });
529
529
 
530
- it('inspects an unbound local checkout and passes the durable store', async () => {
530
+ it('hydrates an unbound checkout under the unbound revision and passes the durable store', async () => {
531
531
  const hydrateBoundCheckout = vi.fn(async () => ({ status: 'not-needed', submodules: [], lfs: [] }));
532
532
  const worktreeBootstrapStore = { read: vi.fn(), write: vi.fn(), remove: vi.fn() };
533
533
  gitLibraries.createWorktree.mockImplementationOnce(async (_directory, _input, options) => {
@@ -556,7 +556,7 @@ describe('contributor worktree authority', () => {
556
556
  directory: '/repo-local',
557
557
  parentRemoteName: '',
558
558
  parentEndpoint: undefined,
559
- repositoryAuthority: null,
559
+ repositoryAuthority: { repositoryId: 'repo_one', bindingRevision: 0, configRevision: 'config_one' },
560
560
  });
561
561
  });
562
562