@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
@@ -0,0 +1,155 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { WorktreeCreationError, createWorktreeCreation } from './worktree-creation.js';
3
+
4
+ const sourceRequest = {
5
+ context: {
6
+ provider: 'github', instance: 'github.com', directory: '/repo', repositoryId: 'repo_one',
7
+ accountId: 'account_one', bindingRevision: 2, primaryRemote: 'origin',
8
+ },
9
+ project: { id: 'acme/app', owner: 'acme', name: 'app' }, number: 42,
10
+ expectedHeadSha: 'a'.repeat(40), requestedRemoteName: 'pr-alice',
11
+ };
12
+ const resolvedSource = {
13
+ context: sourceRequest.context,
14
+ targetProject: sourceRequest.project,
15
+ sourceProject: { id: 'Alice/app', owner: 'Alice', name: 'app' },
16
+ number: 42, headRef: 'refs/heads/feature', headSha: 'a'.repeat(40),
17
+ requestedRemoteName: 'pr-alice', endpoint: 'https://github.com/Alice/app.git',
18
+ classification: 'contributor-fork',
19
+ };
20
+ const validAccount = {
21
+ credentialId: 'account_one', credentialRevision: 3, providerUserId: 'github.com#42', status: 'valid',
22
+ };
23
+
24
+ const setup = (overrides = {}) => {
25
+ const libraries = {
26
+ createWorktree: vi.fn(async () => ({ path: '/worktrees/feature', branch: 'feature' })),
27
+ validateWorktreeCreate: vi.fn(async () => ({ ok: false, errors: [{ code: 'contributor_transfer_unavailable', message: 'transfer first' }] })),
28
+ getRepositoryRemoteUrls: vi.fn(async () => []),
29
+ ...overrides.libraries,
30
+ };
31
+ const dependencies = {
32
+ getGitLibraries: async () => libraries,
33
+ networkOperations: {
34
+ hydrateBoundCheckout: vi.fn(),
35
+ transferContributorHead: vi.fn(async () => ({ state: 'succeeded' })),
36
+ },
37
+ getSourceControlBinding: vi.fn(async () => ({ revision: 0, repository: { repositoryId: 'repo_one' }, binding: null })),
38
+ contributorProvenance: { compareAndSwap: vi.fn() },
39
+ resolveChangeRequestSource: vi.fn(async () => resolvedSource),
40
+ createHttpsCredentialReference: vi.fn(() => 'credential_reference'),
41
+ resolveSourceControlAccount: vi.fn(async () => validAccount),
42
+ worktreeBootstrapStore: { read: vi.fn(), write: vi.fn() },
43
+ ...overrides.dependencies,
44
+ };
45
+ return { libraries, dependencies, creation: createWorktreeCreation(dependencies) };
46
+ };
47
+
48
+ const request = { mode: 'existing', branchName: 'feature', worktreeName: 'feature', changeRequestSource: sourceRequest };
49
+
50
+ describe('worktree creation from a change request', () => {
51
+ it('creates a contributor worktree from the server-resolved head only', async () => {
52
+ const { libraries, dependencies, creation } = setup();
53
+
54
+ await expect(creation.create('/repo', request)).resolves.toEqual({ path: '/worktrees/feature', branch: 'feature' });
55
+
56
+ expect(dependencies.resolveChangeRequestSource).toHaveBeenCalledExactlyOnceWith(sourceRequest);
57
+ expect(dependencies.networkOperations.transferContributorHead).toHaveBeenCalledExactlyOnceWith({
58
+ directory: '/repo', sourceRequest, source: { ...resolvedSource, requestedRemoteName: 'pr-alice' },
59
+ destinationRef: 'refs/remotes/pr-alice/feature', credentialId: 'credential_reference',
60
+ });
61
+ const [, input, options] = libraries.createWorktree.mock.calls[0];
62
+ expect(input).toMatchObject({
63
+ mode: 'existing', branchName: 'feature', worktreeName: 'feature',
64
+ contributorFork: true, changeRequestTransfer: true, contributorTransferComplete: true,
65
+ ensureRemoteName: 'pr-alice', ensureRemoteUrl: 'https://github.com/Alice/app.git',
66
+ existingBranch: 'remotes/pr-alice/feature', expectedRevision: 'a'.repeat(40), setUpstream: false,
67
+ });
68
+ expect(input.changeRequestSource).toBeUndefined();
69
+ expect(options).toMatchObject({
70
+ contributorProvenance: dependencies.contributorProvenance,
71
+ contributorSource: { ...resolvedSource, requestedRemoteName: 'pr-alice' },
72
+ bootstrapStore: dependencies.worktreeBootstrapStore,
73
+ });
74
+ });
75
+
76
+ it('refuses contributor authority supplied by the caller before reading anything', async () => {
77
+ const { libraries, dependencies, creation } = setup();
78
+
79
+ await expect(creation.create('/repo', { ...request, ensureRemoteUrl: 'https://attacker.example/app.git' }))
80
+ .rejects.toMatchObject({ status: 400, code: 'INVALID_CONTRIBUTOR_WORKTREE' });
81
+ expect(dependencies.resolveChangeRequestSource).not.toHaveBeenCalled();
82
+ expect(libraries.createWorktree).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('refuses a remote name that already points elsewhere, before any transfer', async () => {
86
+ const { libraries, dependencies, creation } = setup({ libraries: {
87
+ validateWorktreeCreate: vi.fn(async () => ({
88
+ ok: false,
89
+ errors: [{ code: 'remote_name_collision', message: 'Remote pr-alice already exists with a different endpoint' }],
90
+ })),
91
+ } });
92
+
93
+ const error = await creation.create('/repo', request).catch((caught) => caught);
94
+ expect(error).toBeInstanceOf(WorktreeCreationError);
95
+ expect(error).toMatchObject({
96
+ status: 409, code: 'CONTRIBUTOR_REMOTE_COLLISION', remoteName: 'pr-alice',
97
+ message: 'Remote pr-alice already exists with a different endpoint',
98
+ });
99
+ expect(dependencies.networkOperations.transferContributorHead).not.toHaveBeenCalled();
100
+ expect(libraries.createWorktree).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it('refuses when the account that would fetch the head is not the context\'s exact one', async () => {
104
+ const { libraries, dependencies, creation } = setup({ dependencies: {
105
+ resolveSourceControlAccount: vi.fn(async () => ({ ...validAccount, credentialId: 'other_account' })),
106
+ } });
107
+
108
+ await expect(creation.create('/repo', request)).rejects.toMatchObject({ status: 409, code: 'AUTHENTICATION_REQUIRED' });
109
+ expect(dependencies.networkOperations.transferContributorHead).not.toHaveBeenCalled();
110
+ expect(libraries.createWorktree).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it('reports a failed transfer and creates nothing', async () => {
114
+ const { libraries, creation } = setup({ dependencies: {
115
+ networkOperations: {
116
+ hydrateBoundCheckout: vi.fn(),
117
+ transferContributorHead: vi.fn(async () => ({ state: 'failed', error: { code: 'TIMEOUT', message: 'Git network operation timed out' } })),
118
+ },
119
+ } });
120
+
121
+ await expect(creation.create('/repo', request))
122
+ .rejects.toMatchObject({ status: 409, code: 'TIMEOUT', message: 'Git network operation timed out' });
123
+ expect(libraries.createWorktree).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it('passes a provider refusal through and creates nothing', async () => {
127
+ const stale = Object.assign(new Error('Change request head changed'), { status: 409, code: 'SOURCE_CONTROL_CHANGE_REQUEST_STALE' });
128
+ const { libraries, dependencies, creation } = setup({ dependencies: {
129
+ resolveChangeRequestSource: vi.fn(async () => { throw stale; }),
130
+ } });
131
+
132
+ await expect(creation.create('/repo', request)).rejects.toBe(stale);
133
+ expect(dependencies.networkOperations.transferContributorHead).not.toHaveBeenCalled();
134
+ expect(libraries.createWorktree).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it('refuses where contributor transfer is not served, with no plain worktree instead', async () => {
138
+ const { libraries, creation } = setup({ dependencies: { resolveChangeRequestSource: undefined } });
139
+
140
+ await expect(creation.create('/repo', request)).rejects.toMatchObject({ status: 501, code: 'RUNTIME_UNSUPPORTED' });
141
+ expect(libraries.createWorktree).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('turns a remote collision found while creating into the route\'s collision answer', async () => {
145
+ const collision = Object.assign(new Error('Remote pr-alice already exists with a different endpoint'), {
146
+ code: 'CONTRIBUTOR_REMOTE_COLLISION', status: 409, remoteName: 'pr-alice',
147
+ });
148
+ const { creation } = setup({ libraries: { createWorktree: vi.fn(async () => { throw collision; }) } });
149
+
150
+ await expect(creation.create('/repo', request)).rejects.toMatchObject({
151
+ status: 409, code: 'CONTRIBUTOR_REMOTE_COLLISION', remoteName: 'pr-alice',
152
+ message: 'Contributor remote name is already used by a different endpoint',
153
+ });
154
+ });
155
+ });
@@ -44,6 +44,7 @@
44
44
 
45
45
  - `getOctokitOrNull()`: current Octokit or `null`.
46
46
  - `getOctokitForAccountId(accountId)`: exact persisted or verified CLI Octokit context with credential revision and provider-user identity. It never falls back to another account. A `github.com#cli:` id resolves only while the user has switched to the gh account (`isGhCliActive()`); the gh token is otherwise read only to list the gh account in Settings.
47
+ - `getGitHubCliCredential(accountId, revision?)`: the same gh account as a credential record shaped like a persisted one (`credentialId`, `credentialRevision: 1`, `providerUserId`, `accessToken`, `status: 'valid'`), under the same consent rule plus a check that gh is still signed in as that user. The server's source-control account resolver falls back to it after `getGitHubAuthByAccountId`, so Git transport (fork PR worktrees, managed clones, the credential helper) accepts the gh account that reads already use.
47
48
 
48
49
  ### Repo
49
50
 
@@ -70,6 +71,9 @@
70
71
  - Canonical reads use the exact account and primary remote returned by validation. A GitHub `401` invalidates only that account. `403` and network failures remain read errors and do not change account state.
71
72
  - Route, PR discovery, and repository metadata caches use an opaque digest of both account ID and credential. Canonical status cache keys also include the trusted repository ID and binding revision, so path replacement and rebinding cannot reuse a warm response. Exact accounts and repository bindings are resolved before cache reads, and bound repository validation always precedes fork metadata cache reads.
72
73
  - Canonical PR create, update, merge, and ready routes validate the complete mutation context before credential, Git, cache, or provider access. They use only the validated account and trusted primary remote, require the target and source to belong to the provider-reported fork network, and return durable mutation receipts. Retired `/api/github/pr/*` mutations never enter this flow.
74
+ - Comments and reviews take the same path: `/pr/comment` (a pull request's conversation, through the issues comments API), `/pr/review` (`approve` or `request-changes` for the target's `headSha`, the commit the user read, so a push since refuses it as a changed target; requesting changes needs text, which GitHub posts as the review body) and `/issues/comment` (refuses a number GitHub reports as a pull request). Record kinds are `change-request-comment`, `change-request-review` and `issue-comment`. A comment or review interrupted after dispatch stays `outcome-unknown`: nothing but its private text would tell a lost write from one never sent, and that text is not persisted. A `403`/`404`/`422` refusal answers with GitHub's own reason (`SOURCE_CONTROL_MUTATION_REJECTED`), such as a review of the user's own pull request; it is not stored.
75
+ - `/pr/state` and `/issues/state` close or reopen (`state: 'open' | 'closed'`; record kinds `change-request-state`, `issue-state`). The issue route refuses a pull request number. Restart reconciliation reads the item: the asked state is success, the other open/closed state a definite not-applied failure. A merged pull request reads `closed` and GitHub refuses to reopen it with its own reason.
76
+ - `/pr/labels`, `/issues/labels` and `/pr/reviewers` set the whole set, deduplicated and sorted so a repeat digests the same (record kinds `change-request-labels`, `issue-labels`, `change-request-reviewers`). Labels go through `issues.setLabels`; reviewers are compared with the requested ones and added, then removed, in two calls: asking again for the same set repairs a failure between them. Reconciliation succeeds only when the current set matches; anything else stays `outcome-unknown`, since another edit may have moved it. `GET /references/labels` and `/references/reviewers` (repository labels, assignable users, one page of 100 each) check the repository against the bound network like `/references/detail`. The detail's `pull.reviewers` lists requested users; team requests are left out. `pull.checks` summarizes the head commit's check contexts with `summarizeCheckContexts`, the same counting as the list statuses, and is null for a closed or merged PR.
73
77
  - Canonical mutations resolve the exact local credential and its revision before inspecting durable state. The exact credential ID and revision remain in authorization and digest input, while the route derives the provider-user ID from that resolved credential and passes it separately as receipt actor `providerAccountId` and audit identity. A client-supplied provider-user ID is never accepted as authority and cannot affect either value. Terminal records replay their compact result or stored error without provider repository preflight, but only after exact credential resolution. Running records lazily read only the provider state needed for reconciliation and never repeat a write. New keys retain the full strict preflight before claim and the single provider write. Changed credential, revision, input, or binding authority conflicts. Provider `4xx` rejections are definite failures; `5xx` and statusless transport failures after dispatch remain `outcome-unknown`.
74
78
  - Cross-process execution exclusion, lock errors, and stopped-writer orphan recovery follow the [source-control lock contract](../source-control/DOCUMENTATION.md#cross-process-locks-and-manual-recovery). A live or crashed owner's execution lock blocks reconciliation; restarting alone does not remove it.
75
79
  - Durable mutation records keep the exact credential account actor used for replay authorization, binding authority, server-resolved project and PR coordinates, an input digest, and compact public results only. Receipt and audit actor metadata use the separately verified provider-user ID. PR title/body text and raw provider payloads are not persisted or logged.
@@ -113,14 +117,15 @@ that page, so callers cannot mistake a partial page for a complete one.
113
117
  - It reads local git status and remotes first.
114
118
  - It ranks remotes in this order: explicit remote, tracking remote, `origin`, `upstream`, then the rest.
115
119
  - It resolves those remotes into GitHub repos.
120
+ - The ranked-first remote is the branch's source unless the worktree was checked out from a contributor's fork PR. Such a worktree deliberately has no upstream, so `/pr/status` reads its contributor provenance (`readContributorProvenance`, supplied by the server runtime) and, when the provenance's source ref is this branch, passes the fork remote as `sourceRemoteName`. That remote, not the primary one, is then the only source, so the open PR from the fork is found. Unreadable provenance resolves as for any other branch.
116
121
  - It expands each repo through `parent` and `source` so PRs in upstream repos can still be found.
117
122
  - It skips PR lookup when the current branch matches that repo's default branch.
118
123
  - It first searches for **open** PRs by likely source owner plus exact head branch.
119
124
  - If that fails, it falls back to broader GitHub search for open PRs on the branch name.
120
125
  - An **open PR from any candidate repo always wins** over a closed/merged one, so a merged fork PR can never hide an open upstream PR for the same head.
121
- - Only when no target has an open PR does it return the branch's newest closed/merged PR, as history — and only when that PR's head commit is an ancestor of the checkout's `HEAD`. History is matched by branch name, and names get reused: a fresh worktree cut from the default branch under a name that was merged before must not inherit the old PR.
122
- - History is looked up **only for the ranked-first remote and the branch's own name** — the repo it actually pushes to. Live status is worth searching the whole fork network for; history is not, and asking every target for it multiplies serial GitHub calls until the route hits its `12s` resolve timeout and returns no status at all.
123
- - The history answer is remembered per repo+branch so discovery polls do not re-query it: a found closed/merged record for `6h`, and "no history yet" for `10m`. A found record only changes if a second PR appears on the same head, and while that one is open the open-PR path wins without ever reading this cache.
126
+ - Only when no target has an open PR does it return the branch's newest closed/merged PR, as history. Its head commit must be an ancestor of the checkout's `HEAD`, or have a patch-equivalent commit there after a rebase. Missing old objects cannot prove that association. History is matched by branch name, and names get reused: a fresh worktree cut from the default branch under a name that was merged before must not inherit the old PR.
127
+ - History is looked up only for a non-default branch's own name and the ranked-first remote's fork network: its repository plus its `parent` and `source` targets. This restores merged fork PRs hosted by an upstream repository without querying unrelated contributor remotes, while terminal history never attaches to a target's default branch. A contributor fork PR worktree also asks its ranked-first remote, where that PR merges. The primary network has at most three targets, so history remains bounded instead of fanning out across every configured remote.
128
+ - The history answer is remembered per repo, branch, and ordered primary source-owner set so discovery polls do not re-query it: a found closed/merged record for `6h`, and "no history yet" for `10m`. Scoping by ordered source owners prevents cache misses and result confusion when searching the same fork branch from different direct-upstream or fork-origin remotes. A found record only changes if a second PR appears on the same head, and while that one is open the open-PR path wins without ever reading this cache.
124
129
  - Creating, merging, or closing a PR invalidates both the shared repo pull list and that remembered history.
125
130
  - The route skips the checks summary and the merge-permission lookup for a closed/merged PR: neither is actionable, and both cost extra GitHub calls.
126
131
  - `403` and `404` during repo lookups are treated as expected gaps, not hard errors.
@@ -136,11 +141,11 @@ that page, so callers cannot mistake a partial page for a complete one.
136
141
 
137
142
  ## Reference picker lists
138
143
 
139
- - `GET /api/source-control/github/references?<read context>&kind=issue|pull&filter=&query=&cursor=` takes the same bound read context as every canonical read and answers one page (30) of the project's issues or PRs across its repo network, read with that context's account (origin plus fork parent/source): `{ connected, repo, items, cursor, hasMore, total }`. `repo: null` means the project has no GitHub remote.
140
- - One GraphQL `search` document per page, open items newest-activity first. `filter` adds `assignee:@me` (`assigned`), `author:@me` (`created`) or `review-requested:@me` (`reviewRequested`); anything else is `open`. User text is appended as is, so GitHub qualifiers work; state and sort are added only when the text sets none.
144
+ - `GET /api/source-control/github/references?<read context>&kind=issue|pull&state=&people=&query=&cursor=` takes the same bound read context as every canonical read and answers one page (30) of the project's issues or PRs across its repo network, read with that context's account (origin plus fork parent/source): `{ connected, repo, items, cursor, hasMore, total }`. `repo: null` means the project has no GitHub remote.
145
+ - One GraphQL `search` document per page, newest activity first. `state` (`open`, the default, `closed`, `merged`, `all`) and `people` (`any`, the default, `assigned` → `assignee:@me`, `created` → `author:@me`, `reviewRequested` → `review-requested:@me`) combine; an issue list reads `merged` as closed and ignores review requests. User text is appended as is, so GitHub qualifiers work; state and sort are added only when the text sets none.
141
146
  - A pasted `#123`, `123` or github.com issue/PR link skips search and reads that number with `issueOrPullRequest` in every network repo (a link: only its own repo), whatever the tab's kind.
142
147
  - Each item carries what a row and the preview show: state (issues `open` / `completed` / `not_planned`, PRs `open` / `closed` / `merged`), author, labels, comment count, the body cut at 20 000 characters (`bodyTruncated`), and for a PR draft, head/base and head repo (with `cloneUrl` for fork worktrees).
143
- - `GET /api/source-control/github/references/detail?<read context>&owner=&repo=&number=` answers what the preview adds for one item: the newest 50 comments, oldest first, plus for a PR its reviews (a review's text or verdict, and each line comment with `path`/`line`), size and review decision. Its checks come from `references/status`, the same answer that colours it. The repo must be in the project's network. These fields stay off the page query: on a 30-PR page size, review decision and checks together ran past GitHub's 10 s search timeout (measured 2026-10-01), while one item answers in about a second.
148
+ - `GET /api/source-control/github/references/detail?<read context>&owner=&repo=&number=` answers what the preview adds for one item: the newest 50 comments, oldest first, plus for a PR its reviews (a review's text or verdict, and each line comment with `path`/`line`), its newest 50 commits with `commitTotal`, size and review decision. Its checks come from `references/status`, the same answer that colours it. The repo must be in the project's network. These fields stay off the page query: on a 30-PR page size, review decision and checks together ran past GitHub's 10 s search timeout (measured 2026-10-01), while one item answers in about a second.
144
149
  - `GET /api/source-control/github/references/status?<read context>&pulls=owner/repo#n,...` answers what colours the PRs a picker page shows, at most 30: `{ connected, statuses: [{ owner, repo, number, checks, mergeable, mergeableState }] }` from `fetchPrSummaries`, the sidebar's summaries, so the picker and the sidebar colour a PR the same way. Every repo must be in the project's network. A PR GitHub could not resolve is left out. Mergeability is most of its cost (30 PRs: about 2 s with checks only, 4 to 7 s with mergeability, measured 2026-10-06), so it is a request after the page, not part of it.
145
150
  - Failures are errors, never an empty page; GraphQL `RATE_LIMITED` records the shared cooldown and answers `503`. The client cache lives in `packages/ui/src/components/references/`.
146
151
  - Attaching still reads full context through the canonical `issues/get` + `issues/comments` and `pulls/context`.
@@ -1,3 +1,4 @@
1
+ import { isPlainObject, isString } from '../shared/guards.js';
1
2
  import fs from 'node:fs/promises';
2
3
  import fsSync from 'node:fs';
3
4
  import path from 'node:path';
@@ -18,10 +19,6 @@ const DEFAULT_GITHUB_CLIENT_ID = 'Ov23lizomPOC3eFYo56r';
18
19
  const DEFAULT_GITHUB_SCOPES = 'repo read:org workflow read:user user:email';
19
20
  export const GH_CLI_ACCOUNT_ID = 'gh-cli';
20
21
 
21
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
22
- const isPlainObject = (value) => value === Object(value)
23
- && !Array.isArray(value)
24
- && Object.getPrototypeOf(value) === Object.prototype;
25
22
  const exactKeys = (value, required, optional = []) => {
26
23
  const allowed = new Set([...required, ...optional]);
27
24
  return required.every((key) => Object.hasOwn(value, key))
@@ -177,6 +174,7 @@ function publicCredential(credential, state) {
177
174
  };
178
175
  }
179
176
 
177
+ /** @public Exposed to integration tests that dynamically import the auth module. */
180
178
  export function createGitHubAuthStore({ filePath, fsImpl = fs, lockWaitMs = 2_000 } = {}) {
181
179
  if (!validText(filePath)) throw new TypeError('GitHub auth file path is required');
182
180
  let transactions = Promise.resolve();
@@ -1,3 +1,5 @@
1
+ import { isString } from '../shared/guards.js';
2
+
1
3
  const DEVICE_CODE_URL = 'https://github.com/login/device/code';
2
4
  const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
3
5
  const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
@@ -11,8 +13,6 @@ const encodeForm = (params) => {
11
13
  return body.toString();
12
14
  };
13
15
 
14
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
15
-
16
16
  async function postForm(url, params, { fetch: fetchImpl = fetch, timeoutMs = 10_000 } = {}) {
17
17
  const response = await fetchImpl(url, {
18
18
  method: 'POST',
@@ -17,3 +17,4 @@ function fetchGhCliToken() {
17
17
  export function getGhCliToken() {
18
18
  return fetchGhCliToken();
19
19
  }
20
+
@@ -108,6 +108,37 @@ export async function getOctokitOrNull() {
108
108
  return createOctokit(token, accountId);
109
109
  }
110
110
 
111
+ /**
112
+ * The gh CLI account as a credential record shaped like a persisted one, for
113
+ * Git transport. Same consent rule as getOctokitForAccountId: only while the
114
+ * user has switched to the gh account, and only when gh is still signed in
115
+ * as that same GitHub user. The CLI credential has no revisions beyond 1.
116
+ */
117
+ export async function getGitHubCliCredential(accountId, revision) {
118
+ if (!accountId.startsWith('github.com#cli:') || !isGhCliActive() || isGhCliDisabled()) return null;
119
+ if (revision !== undefined && revision !== 1) return null;
120
+ const token = getGhCliToken();
121
+ if (!token) return null;
122
+ let response;
123
+ try {
124
+ response = await createOctokit(token, accountId).rest.users.getAuthenticated();
125
+ } catch (error) {
126
+ if (error?.status === 401) return null;
127
+ throw error;
128
+ }
129
+ if (githubCliAccountId(response.data.id) !== accountId) return null;
130
+ return {
131
+ credentialId: accountId,
132
+ accountId,
133
+ credentialRevision: 1,
134
+ providerUserId: githubAccountId(response.data.id),
135
+ accessToken: token,
136
+ source: 'cli',
137
+ status: 'valid',
138
+ user: response.data,
139
+ };
140
+ }
141
+
111
142
  export async function getOctokitForAccountId(accountId, options = {}) {
112
143
  const auth = await getGitHubAuthByAccountId(accountId);
113
144
  if (auth) {
@@ -1,5 +1,5 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { getRemotes, getTrackingBranch, isAncestorOfHead } from '../git/index.js';
2
+ import { getRemotes, getTrackingBranch, isAncestorOfHead, isPatchEquivalentOfHead } from '../git/index.js';
3
3
  import { resolveGitHubRepoFromDirectory } from './repo/index.js';
4
4
  import { noteIfGitHubRateLimit } from './rate-limit.js';
5
5
  import { getOctokitCacheIdentity } from './octokit.js';
@@ -596,14 +596,25 @@ const isTerminalPr = (pr) => Boolean(pr) && (pr.state === 'closed' || Boolean(pr
596
596
  // fresh worktree called `feature` cut from the default branch would inherit
597
597
  // the merged PR of last month's `feature`. The PR only belongs to this checkout
598
598
  // when the commit it was merged or closed at is part of the checkout's history.
599
- // `isAncestor` is the git check, replaceable so the tests need no git repository.
600
- const isHistoricalPrOfCheckout = async (directory, pr, { isAncestor = isAncestorOfHead } = {}) => {
599
+ // A rebase can preserve its patch while replacing its commit. Both checks are
600
+ // injectable so focused tests need no git repository.
601
+ const isHistoricalPrOfCheckout = async (directory, pr, {
602
+ isAncestor = isAncestorOfHead,
603
+ isPatchEquivalent = isPatchEquivalentOfHead,
604
+ } = {}) => {
601
605
  const headSha = normalizeText(pr?.head?.sha);
602
606
  if (!headSha) {
603
607
  return false;
604
608
  }
605
609
  try {
606
- return await isAncestor(directory, headSha);
610
+ if (await isAncestor(directory, headSha)) {
611
+ return true;
612
+ }
613
+ } catch {
614
+ return false;
615
+ }
616
+ try {
617
+ return await isPatchEquivalent(directory, headSha);
607
618
  } catch {
608
619
  return false;
609
620
  }
@@ -620,10 +631,10 @@ export { isHistoricalPrOfCheckout };
620
631
  * the same head. The caller must prefer an open PR from ANY target over a
621
632
  * historical one — otherwise a merged fork PR hides an open upstream PR.
622
633
  *
623
- * `includeHistory` is off by default and must stay that way for secondary
624
- * targets. Live status is worth searching the whole fork network for; history
625
- * is not, and doing it per target multiplied the serial GitHub calls until the
626
- * route hit its resolve timeout and reported no status at all.
634
+ * `includeHistory` is off by default and is only enabled for primary-source
635
+ * targets. Live status is worth searching every configured remote; history is
636
+ * limited to the branch's primary fork network so unrelated remotes cannot
637
+ * multiply serial GitHub calls until the route hits its resolve timeout.
627
638
  */
628
639
  const findBranchPrCandidates = async ({ octokit, target, branch, sourceCandidates, force = false, coverage = null, includeHistory = false }) => {
629
640
  const matcher = buildSourceMatcher(sourceCandidates);
@@ -659,7 +670,7 @@ const findBranchPrCandidates = async ({ octokit, target, branch, sourceCandidate
659
670
  return { open: null, historical: null };
660
671
  }
661
672
 
662
- const historicalKey = scopedCacheKey(octokit, `${normalizeRepoKey(target.repo?.owner, target.repo?.repo)}::${branch}`);
673
+ const historicalKey = scopedCacheKey(octokit, `${normalizeRepoKey(target.repo?.owner, target.repo?.repo)}::${branch}::${sourceOwners.map(normalizeLower).filter(Boolean).join(',')}`);
663
674
  if (historicalKey && includeHistory && !force && openListWasComplete) {
664
675
  const cached = _historicalPrCache.get(historicalKey);
665
676
  if (isHistoricalPrCacheFresh(cached)) {
@@ -707,7 +718,7 @@ const findBranchPrCandidates = async ({ octokit, target, branch, sourceCandidate
707
718
  // Exported for focused unit tests of open-versus-historical branch matching.
708
719
  export { findBranchPrCandidates };
709
720
 
710
- export async function resolveGitHubPrStatus({ octokit, directory, branch, remoteName, force = false }) {
721
+ export async function resolveGitHubPrStatus({ octokit, directory, branch, remoteName, sourceRemoteName = null, force = false }) {
711
722
  // A deleted worktree can still have a session in the sidebar that keeps
712
723
  // requesting its PR status. Bail before touching git or GitHub for a
713
724
  // directory that no longer exists — otherwise every poll spends a git call
@@ -755,10 +766,15 @@ export async function resolveGitHubPrStatus({ octokit, directory, branch, remote
755
766
  // forks — are places to look for an open PR, but their `owner:branch`
756
767
  // heads are unrelated branches that merely share a name; treating them as
757
768
  // sources made a fork's closed `main` PR show up on the local main.
769
+ // A branch checked out from a contributor's fork PR comes from that fork's
770
+ // remote, which the caller knows from the worktree's provenance.
758
771
  const primaryRemoteName = resolvedTargets[0]?.remoteName ?? null;
759
- const sourceCandidates = resolvedTargets.filter(
760
- (target) => target.remoteName === primaryRemoteName,
761
- );
772
+ const contributorSources = sourceRemoteName
773
+ ? resolvedTargets.filter((target) => target.remoteName === sourceRemoteName)
774
+ : [];
775
+ const sourceCandidates = contributorSources.length > 0
776
+ ? contributorSources
777
+ : resolvedTargets.filter((target) => target.remoteName === primaryRemoteName);
762
778
  // When every consulted repo list was complete, a no-PR result is
763
779
  // authoritative and the expensive Search API fallback is pointless.
764
780
  const coverage = { authoritative: true };
@@ -786,11 +802,12 @@ export async function resolveGitHubPrStatus({ octokit, directory, branch, remote
786
802
  continue;
787
803
  }
788
804
 
789
- // History is only asked of the branch's own repo and its own name: the
790
- // ranked-first target is the remote this branch actually pushes to.
791
- // Searching the rest of the fork network for history would multiply
792
- // serial GitHub calls for no additional user-visible information.
793
- const isPrimaryAssociation = target === resolvedTargets[0] && candidateBranch === branchCandidates[0];
805
+ // History is only asked for the branch's own name and its primary remote
806
+ // network. That includes a fork's parent/source, where a fork-owned PR
807
+ // may have been merged, while excluding unrelated contributor remotes.
808
+ // A contributor fork checkout's PR merges into the primary remote, which
809
+ // is not among its source candidates.
810
+ const isPrimaryAssociation = (target === resolvedTargets[0] || sourceCandidates.includes(target)) && candidateBranch === branchCandidates[0] && candidateBranch !== defaultBranch;
794
811
 
795
812
  const { open, historical } = await findBranchPrCandidates({
796
813
  octokit,