@dsh-cove/dsh-web-all 0.0.0-stage → 0.4.3

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 (912) hide show
  1. package/LICENSE +202 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +201 -2
  4. package/README.zh.md +202 -0
  5. package/cordis.patch.yml +59 -0
  6. package/lib/client.js +25413 -0
  7. package/lib/client.js.map +1 -0
  8. package/lib/index.js +20024 -0
  9. package/lib/invariant.js +6 -0
  10. package/lib/types/client/compat.d.ts +29 -0
  11. package/lib/types/client/compat.d.ts.map +1 -0
  12. package/lib/types/client/compat.js +69 -0
  13. package/lib/types/client/conversation/git-branch.d.ts +13 -0
  14. package/lib/types/client/conversation/git-branch.d.ts.map +1 -0
  15. package/lib/types/client/conversation/git-branch.js +121 -0
  16. package/lib/types/client/conversation/git-graph/api.d.ts +42 -0
  17. package/lib/types/client/conversation/git-graph/api.d.ts.map +1 -0
  18. package/lib/types/client/conversation/git-graph/api.js +72 -0
  19. package/lib/types/client/conversation/git-graph/branch-chip.d.ts +47 -0
  20. package/lib/types/client/conversation/git-graph/branch-chip.d.ts.map +1 -0
  21. package/lib/types/client/conversation/git-graph/branch-chip.js +268 -0
  22. package/lib/types/client/conversation/git-graph/branch-popover.d.ts +32 -0
  23. package/lib/types/client/conversation/git-graph/branch-popover.d.ts.map +1 -0
  24. package/lib/types/client/conversation/git-graph/branch-popover.js +108 -0
  25. package/lib/types/client/conversation/git-graph/chip.d.ts +24 -0
  26. package/lib/types/client/conversation/git-graph/chip.d.ts.map +1 -0
  27. package/lib/types/client/conversation/git-graph/chip.js +15 -0
  28. package/lib/types/client/conversation/git-graph/create-branch-dialog.d.ts +30 -0
  29. package/lib/types/client/conversation/git-graph/create-branch-dialog.d.ts.map +1 -0
  30. package/lib/types/client/conversation/git-graph/create-branch-dialog.js +52 -0
  31. package/lib/types/client/conversation/git-graph/error-copy.d.ts +17 -0
  32. package/lib/types/client/conversation/git-graph/error-copy.d.ts.map +1 -0
  33. package/lib/types/client/conversation/git-graph/error-copy.js +44 -0
  34. package/lib/types/client/conversation/git-graph/graph-dialog.d.ts +21 -0
  35. package/lib/types/client/conversation/git-graph/graph-dialog.d.ts.map +1 -0
  36. package/lib/types/client/conversation/git-graph/graph-dialog.js +112 -0
  37. package/lib/types/client/conversation/git-graph/hero-seat.d.ts +53 -0
  38. package/lib/types/client/conversation/git-graph/hero-seat.d.ts.map +1 -0
  39. package/lib/types/client/conversation/git-graph/hero-seat.js +54 -0
  40. package/lib/types/client/conversation/git-graph/locales.d.ts +88 -0
  41. package/lib/types/client/conversation/git-graph/locales.d.ts.map +1 -0
  42. package/lib/types/client/conversation/git-graph/locales.js +85 -0
  43. package/lib/types/client/conversation/git-graph/sse-leader.d.ts +37 -0
  44. package/lib/types/client/conversation/git-graph/sse-leader.d.ts.map +1 -0
  45. package/lib/types/client/conversation/git-graph/sse-leader.js +101 -0
  46. package/lib/types/client/conversation/index.d.ts +43 -0
  47. package/lib/types/client/conversation/index.d.ts.map +1 -0
  48. package/lib/types/client/conversation/index.js +39 -0
  49. package/lib/types/client/conversation/selector-context.d.ts +12 -0
  50. package/lib/types/client/conversation/selector-context.d.ts.map +1 -0
  51. package/lib/types/client/conversation/selector-context.js +11 -0
  52. package/lib/types/client/conversation/session-lane-chip.d.ts +72 -0
  53. package/lib/types/client/conversation/session-lane-chip.d.ts.map +1 -0
  54. package/lib/types/client/conversation/session-lane-chip.js +163 -0
  55. package/lib/types/client/conversation/session-lane-locales.d.ts +22 -0
  56. package/lib/types/client/conversation/session-lane-locales.d.ts.map +1 -0
  57. package/lib/types/client/conversation/session-lane-locales.js +29 -0
  58. package/lib/types/client/conversation/subagent-model-dom.d.ts +16 -0
  59. package/lib/types/client/conversation/subagent-model-dom.d.ts.map +1 -0
  60. package/lib/types/client/conversation/subagent-model-dom.js +138 -0
  61. package/lib/types/client/conversation/subagent-model.d.ts +17 -0
  62. package/lib/types/client/conversation/subagent-model.d.ts.map +1 -0
  63. package/lib/types/client/conversation/subagent-model.js +156 -0
  64. package/lib/types/client/conversation/worktree/api.d.ts +97 -0
  65. package/lib/types/client/conversation/worktree/api.d.ts.map +1 -0
  66. package/lib/types/client/conversation/worktree/api.js +239 -0
  67. package/lib/types/client/conversation/worktree/dialogs/DeleteWorktreeDialog.d.ts +35 -0
  68. package/lib/types/client/conversation/worktree/dialogs/DeleteWorktreeDialog.d.ts.map +1 -0
  69. package/lib/types/client/conversation/worktree/dialogs/DeleteWorktreeDialog.js +55 -0
  70. package/lib/types/client/conversation/worktree/dialogs/PrCreateDialog.d.ts +27 -0
  71. package/lib/types/client/conversation/worktree/dialogs/PrCreateDialog.d.ts.map +1 -0
  72. package/lib/types/client/conversation/worktree/dialogs/PrCreateDialog.js +49 -0
  73. package/lib/types/client/conversation/worktree/lane-binding.d.ts +46 -0
  74. package/lib/types/client/conversation/worktree/lane-binding.d.ts.map +1 -0
  75. package/lib/types/client/conversation/worktree/lane-binding.js +60 -0
  76. package/lib/types/client/conversation/worktree/lane-menu.d.ts +51 -0
  77. package/lib/types/client/conversation/worktree/lane-menu.d.ts.map +1 -0
  78. package/lib/types/client/conversation/worktree/lane-menu.js +140 -0
  79. package/lib/types/client/conversation/worktree/lane-open.d.ts +126 -0
  80. package/lib/types/client/conversation/worktree/lane-open.d.ts.map +1 -0
  81. package/lib/types/client/conversation/worktree/lane-open.js +140 -0
  82. package/lib/types/client/conversation/worktree/lane-workspace-sweeper.d.ts +79 -0
  83. package/lib/types/client/conversation/worktree/lane-workspace-sweeper.d.ts.map +1 -0
  84. package/lib/types/client/conversation/worktree/lane-workspace-sweeper.js +116 -0
  85. package/lib/types/client/conversation/worktree/lifecycle.d.ts +29 -0
  86. package/lib/types/client/conversation/worktree/lifecycle.d.ts.map +1 -0
  87. package/lib/types/client/conversation/worktree/lifecycle.js +67 -0
  88. package/lib/types/client/conversation/worktree/locales.d.ts +58 -0
  89. package/lib/types/client/conversation/worktree/locales.d.ts.map +1 -0
  90. package/lib/types/client/conversation/worktree/locales.js +86 -0
  91. package/lib/types/client/conversation/worktree/semantic.d.ts +11 -0
  92. package/lib/types/client/conversation/worktree/semantic.d.ts.map +1 -0
  93. package/lib/types/client/conversation/worktree/semantic.js +10 -0
  94. package/lib/types/client/index.d.ts +14 -0
  95. package/lib/types/client/index.d.ts.map +1 -0
  96. package/lib/types/client/index.js +45 -0
  97. package/lib/types/client/lark-inbox/i18n.d.ts +63 -0
  98. package/lib/types/client/lark-inbox/i18n.d.ts.map +1 -0
  99. package/lib/types/client/lark-inbox/i18n.js +119 -0
  100. package/lib/types/client/lark-inbox/wizard.d.ts +19 -0
  101. package/lib/types/client/lark-inbox/wizard.d.ts.map +1 -0
  102. package/lib/types/client/lark-inbox/wizard.js +158 -0
  103. package/lib/types/client/notify/i18n.d.ts +47 -0
  104. package/lib/types/client/notify/i18n.d.ts.map +1 -0
  105. package/lib/types/client/notify/i18n.js +87 -0
  106. package/lib/types/client/settings/family-scope.d.ts +23 -0
  107. package/lib/types/client/settings/family-scope.d.ts.map +1 -0
  108. package/lib/types/client/settings/family-scope.js +30 -0
  109. package/lib/types/client/settings/lark-inbox.d.ts +73 -0
  110. package/lib/types/client/settings/lark-inbox.d.ts.map +1 -0
  111. package/lib/types/client/settings/lark-inbox.js +144 -0
  112. package/lib/types/client/settings/notify.d.ts +66 -0
  113. package/lib/types/client/settings/notify.d.ts.map +1 -0
  114. package/lib/types/client/settings/notify.js +139 -0
  115. package/lib/types/client/settings/primitives-0.2.d.ts +137 -0
  116. package/lib/types/client/settings/primitives-0.2.d.ts.map +1 -0
  117. package/lib/types/client/settings/primitives-0.2.js +8 -0
  118. package/lib/types/client/sidebar/RenderBoundary.d.ts +17 -0
  119. package/lib/types/client/sidebar/RenderBoundary.d.ts.map +1 -0
  120. package/lib/types/client/sidebar/RenderBoundary.js +18 -0
  121. package/lib/types/client/sidebar/agent-browser.d.ts +9 -0
  122. package/lib/types/client/sidebar/agent-browser.d.ts.map +1 -0
  123. package/lib/types/client/sidebar/agent-browser.js +102 -0
  124. package/lib/types/client/sidebar/diff-view.d.ts +58 -0
  125. package/lib/types/client/sidebar/diff-view.d.ts.map +1 -0
  126. package/lib/types/client/sidebar/diff-view.js +223 -0
  127. package/lib/types/client/sidebar/git-workbench.d.ts +41 -0
  128. package/lib/types/client/sidebar/git-workbench.d.ts.map +1 -0
  129. package/lib/types/client/sidebar/git-workbench.js +480 -0
  130. package/lib/types/client/sidebar/icons.d.ts +9 -0
  131. package/lib/types/client/sidebar/icons.d.ts.map +1 -0
  132. package/lib/types/client/sidebar/icons.js +6 -0
  133. package/lib/types/client/sidebar/index.d.ts +19 -0
  134. package/lib/types/client/sidebar/index.d.ts.map +1 -0
  135. package/lib/types/client/sidebar/index.js +12 -0
  136. package/lib/types/client/sidebar/native-panels.d.ts +19 -0
  137. package/lib/types/client/sidebar/native-panels.d.ts.map +1 -0
  138. package/lib/types/client/sidebar/native-panels.js +103 -0
  139. package/lib/types/client/sidebar/page-shell.d.ts +52 -0
  140. package/lib/types/client/sidebar/page-shell.d.ts.map +1 -0
  141. package/lib/types/client/sidebar/page-shell.js +18 -0
  142. package/lib/types/client/sidebar/panels/memoir/api.d.ts +258 -0
  143. package/lib/types/client/sidebar/panels/memoir/api.d.ts.map +1 -0
  144. package/lib/types/client/sidebar/panels/memoir/api.js +135 -0
  145. package/lib/types/client/sidebar/panels/memoir/controller.d.ts +22 -0
  146. package/lib/types/client/sidebar/panels/memoir/controller.d.ts.map +1 -0
  147. package/lib/types/client/sidebar/panels/memoir/controller.js +38 -0
  148. package/lib/types/client/sidebar/panels/memoir/cwd.d.ts +80 -0
  149. package/lib/types/client/sidebar/panels/memoir/cwd.d.ts.map +1 -0
  150. package/lib/types/client/sidebar/panels/memoir/cwd.js +96 -0
  151. package/lib/types/client/sidebar/panels/memoir/i18n.d.ts +21 -0
  152. package/lib/types/client/sidebar/panels/memoir/i18n.d.ts.map +1 -0
  153. package/lib/types/client/sidebar/panels/memoir/i18n.js +288 -0
  154. package/lib/types/client/sidebar/panels/memoir/panel-activation.d.ts +11 -0
  155. package/lib/types/client/sidebar/panels/memoir/panel-activation.d.ts.map +1 -0
  156. package/lib/types/client/sidebar/panels/memoir/panel-activation.js +37 -0
  157. package/lib/types/client/sidebar/panels/memoir/panel.d.ts +26 -0
  158. package/lib/types/client/sidebar/panels/memoir/panel.d.ts.map +1 -0
  159. package/lib/types/client/sidebar/panels/memoir/panel.js +423 -0
  160. package/lib/types/client/sidebar/panels/memoir/styles.d.ts +22 -0
  161. package/lib/types/client/sidebar/panels/memoir/styles.d.ts.map +1 -0
  162. package/lib/types/client/sidebar/panels/memoir/styles.js +774 -0
  163. package/lib/types/client/sidebar/panels/memoir/types.d.ts +7 -0
  164. package/lib/types/client/sidebar/panels/memoir/types.d.ts.map +1 -0
  165. package/lib/types/client/sidebar/panels/memoir/types.js +5 -0
  166. package/lib/types/client/sidebar/panels/ssh/ClusterTab.d.ts +8 -0
  167. package/lib/types/client/sidebar/panels/ssh/ClusterTab.d.ts.map +1 -0
  168. package/lib/types/client/sidebar/panels/ssh/ClusterTab.js +53 -0
  169. package/lib/types/client/sidebar/panels/ssh/HostFormDialog.d.ts +13 -0
  170. package/lib/types/client/sidebar/panels/ssh/HostFormDialog.d.ts.map +1 -0
  171. package/lib/types/client/sidebar/panels/ssh/HostFormDialog.js +110 -0
  172. package/lib/types/client/sidebar/panels/ssh/HostsTab.d.ts +26 -0
  173. package/lib/types/client/sidebar/panels/ssh/HostsTab.d.ts.map +1 -0
  174. package/lib/types/client/sidebar/panels/ssh/HostsTab.js +175 -0
  175. package/lib/types/client/sidebar/panels/ssh/SshPanel.d.ts +14 -0
  176. package/lib/types/client/sidebar/panels/ssh/SshPanel.d.ts.map +1 -0
  177. package/lib/types/client/sidebar/panels/ssh/SshPanel.js +34 -0
  178. package/lib/types/client/sidebar/panels/ssh/TerminalTab.d.ts +18 -0
  179. package/lib/types/client/sidebar/panels/ssh/TerminalTab.d.ts.map +1 -0
  180. package/lib/types/client/sidebar/panels/ssh/TerminalTab.js +207 -0
  181. package/lib/types/client/sidebar/panels/ssh/TransferTab.d.ts +8 -0
  182. package/lib/types/client/sidebar/panels/ssh/TransferTab.d.ts.map +1 -0
  183. package/lib/types/client/sidebar/panels/ssh/TransferTab.js +168 -0
  184. package/lib/types/client/sidebar/panels/ssh/TunnelsTab.d.ts +18 -0
  185. package/lib/types/client/sidebar/panels/ssh/TunnelsTab.d.ts.map +1 -0
  186. package/lib/types/client/sidebar/panels/ssh/TunnelsTab.js +161 -0
  187. package/lib/types/client/sidebar/panels/ssh/api.d.ts +80 -0
  188. package/lib/types/client/sidebar/panels/ssh/api.d.ts.map +1 -0
  189. package/lib/types/client/sidebar/panels/ssh/api.js +333 -0
  190. package/lib/types/client/sidebar/panels/ssh/controller.d.ts +23 -0
  191. package/lib/types/client/sidebar/panels/ssh/controller.d.ts.map +1 -0
  192. package/lib/types/client/sidebar/panels/ssh/controller.js +41 -0
  193. package/lib/types/client/sidebar/panels/ssh/helpers.d.ts +44 -0
  194. package/lib/types/client/sidebar/panels/ssh/helpers.d.ts.map +1 -0
  195. package/lib/types/client/sidebar/panels/ssh/helpers.js +53 -0
  196. package/lib/types/client/sidebar/panels/ssh/locales.d.ts +155 -0
  197. package/lib/types/client/sidebar/panels/ssh/locales.d.ts.map +1 -0
  198. package/lib/types/client/sidebar/panels/ssh/locales.js +312 -0
  199. package/lib/types/client/sidebar/panels/ssh/xterm.css.d.ts +3 -0
  200. package/lib/types/client/sidebar/panels/ssh/xterm.css.d.ts.map +1 -0
  201. package/lib/types/client/sidebar/panels/ssh/xterm.css.js +2 -0
  202. package/lib/types/client/sidebar/right-pane-i18n.d.ts +121 -0
  203. package/lib/types/client/sidebar/right-pane-i18n.d.ts.map +1 -0
  204. package/lib/types/client/sidebar/right-pane-i18n.js +239 -0
  205. package/lib/types/client/sidebar/right-pane.d.ts +27 -0
  206. package/lib/types/client/sidebar/right-pane.d.ts.map +1 -0
  207. package/lib/types/client/sidebar/right-pane.js +112 -0
  208. package/lib/types/client/sidebar/scope.d.ts +17 -0
  209. package/lib/types/client/sidebar/scope.d.ts.map +1 -0
  210. package/lib/types/client/sidebar/scope.js +41 -0
  211. package/lib/types/client/trust.d.ts +78 -0
  212. package/lib/types/client/trust.d.ts.map +1 -0
  213. package/lib/types/client/trust.js +130 -0
  214. package/lib/types/client/ui-icons.d.ts +19 -0
  215. package/lib/types/client/ui-icons.d.ts.map +1 -0
  216. package/lib/types/client/ui-icons.js +33 -0
  217. package/lib/types/core/git-graph/commands.d.ts +36 -0
  218. package/lib/types/core/git-graph/commands.d.ts.map +1 -0
  219. package/lib/types/core/git-graph/commands.js +122 -0
  220. package/lib/types/core/git-graph/protocol.d.ts +104 -0
  221. package/lib/types/core/git-graph/protocol.d.ts.map +1 -0
  222. package/lib/types/core/git-graph/protocol.js +142 -0
  223. package/lib/types/core/index.d.ts +3 -0
  224. package/lib/types/core/index.d.ts.map +1 -0
  225. package/lib/types/core/index.js +2 -0
  226. package/lib/types/core/lark-inbox/gating.d.ts +21 -0
  227. package/lib/types/core/lark-inbox/gating.d.ts.map +1 -0
  228. package/lib/types/core/lark-inbox/gating.js +17 -0
  229. package/lib/types/core/lark-inbox/queue.d.ts +27 -0
  230. package/lib/types/core/lark-inbox/queue.d.ts.map +1 -0
  231. package/lib/types/core/lark-inbox/queue.js +78 -0
  232. package/lib/types/core/memoir/protocol.d.ts +42 -0
  233. package/lib/types/core/memoir/protocol.d.ts.map +1 -0
  234. package/lib/types/core/memoir/protocol.js +8 -0
  235. package/lib/types/core/no-browser-auth/contract.d.ts +22 -0
  236. package/lib/types/core/no-browser-auth/contract.d.ts.map +1 -0
  237. package/lib/types/core/no-browser-auth/contract.js +9 -0
  238. package/lib/types/core/notify/contract.d.ts +133 -0
  239. package/lib/types/core/notify/contract.d.ts.map +1 -0
  240. package/lib/types/core/notify/contract.js +186 -0
  241. package/lib/types/core/notify/feed.d.ts +116 -0
  242. package/lib/types/core/notify/feed.d.ts.map +1 -0
  243. package/lib/types/core/notify/feed.js +214 -0
  244. package/lib/types/core/playwright-sessions/contracts.d.ts +98 -0
  245. package/lib/types/core/playwright-sessions/contracts.d.ts.map +1 -0
  246. package/lib/types/core/playwright-sessions/contracts.js +68 -0
  247. package/lib/types/core/playwright-sessions/control.d.ts +19 -0
  248. package/lib/types/core/playwright-sessions/control.d.ts.map +1 -0
  249. package/lib/types/core/playwright-sessions/control.js +9 -0
  250. package/lib/types/core/playwright-sessions/index.d.ts +4 -0
  251. package/lib/types/core/playwright-sessions/index.d.ts.map +1 -0
  252. package/lib/types/core/playwright-sessions/index.js +3 -0
  253. package/lib/types/core/playwright-sessions/names.d.ts +4 -0
  254. package/lib/types/core/playwright-sessions/names.d.ts.map +1 -0
  255. package/lib/types/core/playwright-sessions/names.js +84 -0
  256. package/lib/types/core/playwright-sessions/schema.d.ts +4 -0
  257. package/lib/types/core/playwright-sessions/schema.d.ts.map +1 -0
  258. package/lib/types/core/playwright-sessions/schema.js +90 -0
  259. package/lib/types/core/playwright-sessions/wire.d.ts +78 -0
  260. package/lib/types/core/playwright-sessions/wire.d.ts.map +1 -0
  261. package/lib/types/core/playwright-sessions/wire.js +103 -0
  262. package/lib/types/core/remote-access/paths.d.ts +19 -0
  263. package/lib/types/core/remote-access/paths.d.ts.map +1 -0
  264. package/lib/types/core/remote-access/paths.js +18 -0
  265. package/lib/types/core/remote-web-ui/security.d.ts +35 -0
  266. package/lib/types/core/remote-web-ui/security.d.ts.map +1 -0
  267. package/lib/types/core/remote-web-ui/security.js +146 -0
  268. package/lib/types/core/right-pane/protocol.d.ts +25 -0
  269. package/lib/types/core/right-pane/protocol.d.ts.map +1 -0
  270. package/lib/types/core/right-pane/protocol.js +91 -0
  271. package/lib/types/core/schedule-mode/index.d.ts +7 -0
  272. package/lib/types/core/schedule-mode/index.d.ts.map +1 -0
  273. package/lib/types/core/schedule-mode/index.js +6 -0
  274. package/lib/types/core/schedule-mode/native.d.ts +35 -0
  275. package/lib/types/core/schedule-mode/native.d.ts.map +1 -0
  276. package/lib/types/core/schedule-mode/native.js +82 -0
  277. package/lib/types/core/schedule-mode/types.d.ts +177 -0
  278. package/lib/types/core/schedule-mode/types.d.ts.map +1 -0
  279. package/lib/types/core/schedule-mode/types.js +65 -0
  280. package/lib/types/core/schedule-mode/view.d.ts +62 -0
  281. package/lib/types/core/schedule-mode/view.d.ts.map +1 -0
  282. package/lib/types/core/schedule-mode/view.js +4 -0
  283. package/lib/types/core/ssh/protocol.d.ts +215 -0
  284. package/lib/types/core/ssh/protocol.d.ts.map +1 -0
  285. package/lib/types/core/ssh/protocol.js +23 -0
  286. package/lib/types/core/subagent-model/badges.d.ts +73 -0
  287. package/lib/types/core/subagent-model/badges.d.ts.map +1 -0
  288. package/lib/types/core/subagent-model/badges.js +105 -0
  289. package/lib/types/core/subagent-model/model-resolver.d.ts +25 -0
  290. package/lib/types/core/subagent-model/model-resolver.d.ts.map +1 -0
  291. package/lib/types/core/subagent-model/model-resolver.js +60 -0
  292. package/lib/types/core/worktree/invariant.d.ts +62 -0
  293. package/lib/types/core/worktree/invariant.d.ts.map +1 -0
  294. package/lib/types/core/worktree/invariant.js +72 -0
  295. package/lib/types/core/worktree/manifest.d.ts +71 -0
  296. package/lib/types/core/worktree/manifest.d.ts.map +1 -0
  297. package/lib/types/core/worktree/manifest.js +98 -0
  298. package/lib/types/core/worktree/paths.d.ts +60 -0
  299. package/lib/types/core/worktree/paths.d.ts.map +1 -0
  300. package/lib/types/core/worktree/paths.js +118 -0
  301. package/lib/types/core/worktree/pr-create.d.ts +67 -0
  302. package/lib/types/core/worktree/pr-create.d.ts.map +1 -0
  303. package/lib/types/core/worktree/pr-create.js +118 -0
  304. package/lib/types/core/worktree/protocol.d.ts +7 -0
  305. package/lib/types/core/worktree/protocol.d.ts.map +1 -0
  306. package/lib/types/core/worktree/protocol.js +6 -0
  307. package/lib/types/core/worktree/types.d.ts +292 -0
  308. package/lib/types/core/worktree/types.d.ts.map +1 -0
  309. package/lib/types/core/worktree/types.js +14 -0
  310. package/lib/types/core/worktree/workspace-model.d.ts +56 -0
  311. package/lib/types/core/worktree/workspace-model.d.ts.map +1 -0
  312. package/lib/types/core/worktree/workspace-model.js +14 -0
  313. package/lib/types/host/services/access-trust/index.d.ts +54 -0
  314. package/lib/types/host/services/access-trust/index.d.ts.map +1 -0
  315. package/lib/types/host/services/access-trust/index.js +82 -0
  316. package/lib/types/host/services/access-trust/trust.d.ts +70 -0
  317. package/lib/types/host/services/access-trust/trust.d.ts.map +1 -0
  318. package/lib/types/host/services/access-trust/trust.js +187 -0
  319. package/lib/types/host/services/codex-bridge/bridge.d.ts +106 -0
  320. package/lib/types/host/services/codex-bridge/bridge.d.ts.map +1 -0
  321. package/lib/types/host/services/codex-bridge/bridge.js +321 -0
  322. package/lib/types/host/services/codex-bridge/codex-auth.d.ts +108 -0
  323. package/lib/types/host/services/codex-bridge/codex-auth.d.ts.map +1 -0
  324. package/lib/types/host/services/codex-bridge/codex-auth.js +241 -0
  325. package/lib/types/host/services/codex-bridge/dsh-integration.d.ts +58 -0
  326. package/lib/types/host/services/codex-bridge/dsh-integration.d.ts.map +1 -0
  327. package/lib/types/host/services/codex-bridge/dsh-integration.js +120 -0
  328. package/lib/types/host/services/codex-bridge/http.d.ts +43 -0
  329. package/lib/types/host/services/codex-bridge/http.d.ts.map +1 -0
  330. package/lib/types/host/services/codex-bridge/http.js +92 -0
  331. package/lib/types/host/services/codex-bridge/index.d.ts +63 -0
  332. package/lib/types/host/services/codex-bridge/index.d.ts.map +1 -0
  333. package/lib/types/host/services/codex-bridge/index.js +111 -0
  334. package/lib/types/host/services/codex-bridge/loopback.d.ts +25 -0
  335. package/lib/types/host/services/codex-bridge/loopback.d.ts.map +1 -0
  336. package/lib/types/host/services/codex-bridge/loopback.js +68 -0
  337. package/lib/types/host/services/codex-bridge/routes.d.ts +31 -0
  338. package/lib/types/host/services/codex-bridge/routes.d.ts.map +1 -0
  339. package/lib/types/host/services/codex-bridge/routes.js +126 -0
  340. package/lib/types/host/services/family-config.d.ts +71 -0
  341. package/lib/types/host/services/family-config.d.ts.map +1 -0
  342. package/lib/types/host/services/family-config.js +166 -0
  343. package/lib/types/host/services/family-namespaces.d.ts +24 -0
  344. package/lib/types/host/services/family-namespaces.d.ts.map +1 -0
  345. package/lib/types/host/services/family-namespaces.js +30 -0
  346. package/lib/types/host/services/git-graph/index.d.ts +6 -0
  347. package/lib/types/host/services/git-graph/index.d.ts.map +1 -0
  348. package/lib/types/host/services/git-graph/index.js +7 -0
  349. package/lib/types/host/services/git-graph/routes.d.ts +4 -0
  350. package/lib/types/host/services/git-graph/routes.d.ts.map +1 -0
  351. package/lib/types/host/services/git-graph/routes.js +241 -0
  352. package/lib/types/host/services/git-graph/service.d.ts +52 -0
  353. package/lib/types/host/services/git-graph/service.d.ts.map +1 -0
  354. package/lib/types/host/services/git-graph/service.js +301 -0
  355. package/lib/types/host/services/index.d.ts +23 -0
  356. package/lib/types/host/services/index.d.ts.map +1 -0
  357. package/lib/types/host/services/index.js +80 -0
  358. package/lib/types/host/services/lark-inbox/config.d.ts +24 -0
  359. package/lib/types/host/services/lark-inbox/config.d.ts.map +1 -0
  360. package/lib/types/host/services/lark-inbox/config.js +48 -0
  361. package/lib/types/host/services/lark-inbox/gateway.d.ts +53 -0
  362. package/lib/types/host/services/lark-inbox/gateway.d.ts.map +1 -0
  363. package/lib/types/host/services/lark-inbox/gateway.js +171 -0
  364. package/lib/types/host/services/lark-inbox/host-routes.d.ts +22 -0
  365. package/lib/types/host/services/lark-inbox/host-routes.d.ts.map +1 -0
  366. package/lib/types/host/services/lark-inbox/host-routes.js +146 -0
  367. package/lib/types/host/services/lark-inbox/inbox-store.d.ts +39 -0
  368. package/lib/types/host/services/lark-inbox/inbox-store.d.ts.map +1 -0
  369. package/lib/types/host/services/lark-inbox/inbox-store.js +124 -0
  370. package/lib/types/host/services/lark-inbox/index.d.ts +26 -0
  371. package/lib/types/host/services/lark-inbox/index.d.ts.map +1 -0
  372. package/lib/types/host/services/lark-inbox/index.js +135 -0
  373. package/lib/types/host/services/lark-inbox/tools.d.ts +7 -0
  374. package/lib/types/host/services/lark-inbox/tools.d.ts.map +1 -0
  375. package/lib/types/host/services/lark-inbox/tools.js +119 -0
  376. package/lib/types/host/services/lark-inbox/wizard.d.ts +62 -0
  377. package/lib/types/host/services/lark-inbox/wizard.d.ts.map +1 -0
  378. package/lib/types/host/services/lark-inbox/wizard.js +211 -0
  379. package/lib/types/host/services/lifecycle.d.ts +29 -0
  380. package/lib/types/host/services/lifecycle.d.ts.map +1 -0
  381. package/lib/types/host/services/lifecycle.js +93 -0
  382. package/lib/types/host/services/memoir/index.d.ts +40 -0
  383. package/lib/types/host/services/memoir/index.d.ts.map +1 -0
  384. package/lib/types/host/services/memoir/index.js +52 -0
  385. package/lib/types/host/services/memoir/routes.d.ts +30 -0
  386. package/lib/types/host/services/memoir/routes.d.ts.map +1 -0
  387. package/lib/types/host/services/memoir/routes.js +364 -0
  388. package/lib/types/host/services/memoir/settings.d.ts +27 -0
  389. package/lib/types/host/services/memoir/settings.d.ts.map +1 -0
  390. package/lib/types/host/services/memoir/settings.js +120 -0
  391. package/lib/types/host/services/memoir/store.d.ts +35 -0
  392. package/lib/types/host/services/memoir/store.d.ts.map +1 -0
  393. package/lib/types/host/services/memoir/store.js +124 -0
  394. package/lib/types/host/services/memoir/tools.d.ts +8 -0
  395. package/lib/types/host/services/memoir/tools.d.ts.map +1 -0
  396. package/lib/types/host/services/memoir/tools.js +154 -0
  397. package/lib/types/host/services/no-browser-auth/index.d.ts +25 -0
  398. package/lib/types/host/services/no-browser-auth/index.d.ts.map +1 -0
  399. package/lib/types/host/services/no-browser-auth/index.js +42 -0
  400. package/lib/types/host/services/notify/config.d.ts +105 -0
  401. package/lib/types/host/services/notify/config.d.ts.map +1 -0
  402. package/lib/types/host/services/notify/config.js +123 -0
  403. package/lib/types/host/services/notify/feishu-sender.d.ts +120 -0
  404. package/lib/types/host/services/notify/feishu-sender.d.ts.map +1 -0
  405. package/lib/types/host/services/notify/feishu-sender.js +186 -0
  406. package/lib/types/host/services/notify/host-routes.d.ts +69 -0
  407. package/lib/types/host/services/notify/host-routes.d.ts.map +1 -0
  408. package/lib/types/host/services/notify/host-routes.js +112 -0
  409. package/lib/types/host/services/notify/index.d.ts +80 -0
  410. package/lib/types/host/services/notify/index.d.ts.map +1 -0
  411. package/lib/types/host/services/notify/index.js +263 -0
  412. package/lib/types/host/services/playwright-sessions/child.d.ts +76 -0
  413. package/lib/types/host/services/playwright-sessions/child.d.ts.map +1 -0
  414. package/lib/types/host/services/playwright-sessions/child.js +197 -0
  415. package/lib/types/host/services/playwright-sessions/controller.d.ts +25 -0
  416. package/lib/types/host/services/playwright-sessions/controller.d.ts.map +1 -0
  417. package/lib/types/host/services/playwright-sessions/controller.js +104 -0
  418. package/lib/types/host/services/playwright-sessions/index.d.ts +6 -0
  419. package/lib/types/host/services/playwright-sessions/index.d.ts.map +1 -0
  420. package/lib/types/host/services/playwright-sessions/index.js +161 -0
  421. package/lib/types/host/services/playwright-sessions/live-view.d.ts +32 -0
  422. package/lib/types/host/services/playwright-sessions/live-view.d.ts.map +1 -0
  423. package/lib/types/host/services/playwright-sessions/live-view.js +188 -0
  424. package/lib/types/host/services/playwright-sessions/manager.d.ts +87 -0
  425. package/lib/types/host/services/playwright-sessions/manager.d.ts.map +1 -0
  426. package/lib/types/host/services/playwright-sessions/manager.js +203 -0
  427. package/lib/types/host/services/playwright-sessions/profile-gc.d.ts +39 -0
  428. package/lib/types/host/services/playwright-sessions/profile-gc.d.ts.map +1 -0
  429. package/lib/types/host/services/playwright-sessions/profile-gc.js +72 -0
  430. package/lib/types/host/services/playwright-sessions/routes.d.ts +18 -0
  431. package/lib/types/host/services/playwright-sessions/routes.d.ts.map +1 -0
  432. package/lib/types/host/services/playwright-sessions/routes.js +87 -0
  433. package/lib/types/host/services/remote-access/boot-scripts.d.ts +55 -0
  434. package/lib/types/host/services/remote-access/boot-scripts.d.ts.map +1 -0
  435. package/lib/types/host/services/remote-access/boot-scripts.js +109 -0
  436. package/lib/types/host/services/remote-access/index.d.ts +28 -0
  437. package/lib/types/host/services/remote-access/index.d.ts.map +1 -0
  438. package/lib/types/host/services/remote-access/index.js +47 -0
  439. package/lib/types/host/services/remote-access/routes.d.ts +42 -0
  440. package/lib/types/host/services/remote-access/routes.d.ts.map +1 -0
  441. package/lib/types/host/services/remote-access/routes.js +119 -0
  442. package/lib/types/host/services/remote-web-ui/index.d.ts +21 -0
  443. package/lib/types/host/services/remote-web-ui/index.d.ts.map +1 -0
  444. package/lib/types/host/services/remote-web-ui/index.js +325 -0
  445. package/lib/types/host/services/remote-web-ui/server-mode/accounts.d.ts +115 -0
  446. package/lib/types/host/services/remote-web-ui/server-mode/accounts.d.ts.map +1 -0
  447. package/lib/types/host/services/remote-web-ui/server-mode/accounts.js +396 -0
  448. package/lib/types/host/services/remote-web-ui/server-mode/cookie.d.ts +28 -0
  449. package/lib/types/host/services/remote-web-ui/server-mode/cookie.d.ts.map +1 -0
  450. package/lib/types/host/services/remote-web-ui/server-mode/cookie.js +48 -0
  451. package/lib/types/host/services/remote-web-ui/server-mode/dsh-home.d.ts +17 -0
  452. package/lib/types/host/services/remote-web-ui/server-mode/dsh-home.d.ts.map +1 -0
  453. package/lib/types/host/services/remote-web-ui/server-mode/dsh-home.js +40 -0
  454. package/lib/types/host/services/remote-web-ui/server-mode/external-auth.d.ts +73 -0
  455. package/lib/types/host/services/remote-web-ui/server-mode/external-auth.d.ts.map +1 -0
  456. package/lib/types/host/services/remote-web-ui/server-mode/external-auth.js +242 -0
  457. package/lib/types/host/services/remote-web-ui/server-mode/fence.d.ts +23 -0
  458. package/lib/types/host/services/remote-web-ui/server-mode/fence.d.ts.map +1 -0
  459. package/lib/types/host/services/remote-web-ui/server-mode/fence.js +24 -0
  460. package/lib/types/host/services/remote-web-ui/server-mode/gateway-routes.d.ts +89 -0
  461. package/lib/types/host/services/remote-web-ui/server-mode/gateway-routes.d.ts.map +1 -0
  462. package/lib/types/host/services/remote-web-ui/server-mode/gateway-routes.js +768 -0
  463. package/lib/types/host/services/remote-web-ui/server-mode/gateway.d.ts +99 -0
  464. package/lib/types/host/services/remote-web-ui/server-mode/gateway.d.ts.map +1 -0
  465. package/lib/types/host/services/remote-web-ui/server-mode/gateway.js +679 -0
  466. package/lib/types/host/services/remote-web-ui/server-mode/http.d.ts +43 -0
  467. package/lib/types/host/services/remote-web-ui/server-mode/http.d.ts.map +1 -0
  468. package/lib/types/host/services/remote-web-ui/server-mode/http.js +92 -0
  469. package/lib/types/host/services/remote-web-ui/server-mode/integrated-auth.d.ts +42 -0
  470. package/lib/types/host/services/remote-web-ui/server-mode/integrated-auth.d.ts.map +1 -0
  471. package/lib/types/host/services/remote-web-ui/server-mode/integrated-auth.js +73 -0
  472. package/lib/types/host/services/remote-web-ui/server-mode/lan.d.ts +13 -0
  473. package/lib/types/host/services/remote-web-ui/server-mode/lan.d.ts.map +1 -0
  474. package/lib/types/host/services/remote-web-ui/server-mode/lan.js +17 -0
  475. package/lib/types/host/services/remote-web-ui/server-mode/loopback.d.ts +25 -0
  476. package/lib/types/host/services/remote-web-ui/server-mode/loopback.d.ts.map +1 -0
  477. package/lib/types/host/services/remote-web-ui/server-mode/loopback.js +68 -0
  478. package/lib/types/host/services/remote-web-ui/server-mode/posture.d.ts +75 -0
  479. package/lib/types/host/services/remote-web-ui/server-mode/posture.d.ts.map +1 -0
  480. package/lib/types/host/services/remote-web-ui/server-mode/posture.js +116 -0
  481. package/lib/types/host/services/remote-web-ui/server-mode/roles.d.ts +44 -0
  482. package/lib/types/host/services/remote-web-ui/server-mode/roles.d.ts.map +1 -0
  483. package/lib/types/host/services/remote-web-ui/server-mode/roles.js +47 -0
  484. package/lib/types/host/services/remote-web-ui/server-mode/server-boot.d.ts +21 -0
  485. package/lib/types/host/services/remote-web-ui/server-mode/server-boot.d.ts.map +1 -0
  486. package/lib/types/host/services/remote-web-ui/server-mode/server-boot.js +37 -0
  487. package/lib/types/host/services/remote-web-ui/server-mode/sessions.d.ts +111 -0
  488. package/lib/types/host/services/remote-web-ui/server-mode/sessions.d.ts.map +1 -0
  489. package/lib/types/host/services/remote-web-ui/server-mode/sessions.js +298 -0
  490. package/lib/types/host/services/remote-web-ui/server.d.ts +82 -0
  491. package/lib/types/host/services/remote-web-ui/server.d.ts.map +1 -0
  492. package/lib/types/host/services/remote-web-ui/server.js +492 -0
  493. package/lib/types/host/services/resources.d.ts +9 -0
  494. package/lib/types/host/services/resources.d.ts.map +1 -0
  495. package/lib/types/host/services/resources.js +98 -0
  496. package/lib/types/host/services/right-pane/browser.d.ts +42 -0
  497. package/lib/types/host/services/right-pane/browser.d.ts.map +1 -0
  498. package/lib/types/host/services/right-pane/browser.js +106 -0
  499. package/lib/types/host/services/right-pane/index.d.ts +22 -0
  500. package/lib/types/host/services/right-pane/index.d.ts.map +1 -0
  501. package/lib/types/host/services/right-pane/index.js +114 -0
  502. package/lib/types/host/services/right-pane/wire.d.ts +6 -0
  503. package/lib/types/host/services/right-pane/wire.d.ts.map +1 -0
  504. package/lib/types/host/services/right-pane/wire.js +9 -0
  505. package/lib/types/host/services/schedule-mode/config.d.ts +11 -0
  506. package/lib/types/host/services/schedule-mode/config.d.ts.map +1 -0
  507. package/lib/types/host/services/schedule-mode/config.js +15 -0
  508. package/lib/types/host/services/schedule-mode/engine.d.ts +150 -0
  509. package/lib/types/host/services/schedule-mode/engine.d.ts.map +1 -0
  510. package/lib/types/host/services/schedule-mode/engine.js +672 -0
  511. package/lib/types/host/services/schedule-mode/index.d.ts +33 -0
  512. package/lib/types/host/services/schedule-mode/index.d.ts.map +1 -0
  513. package/lib/types/host/services/schedule-mode/index.js +175 -0
  514. package/lib/types/host/services/schedule-mode/routes.d.ts +31 -0
  515. package/lib/types/host/services/schedule-mode/routes.d.ts.map +1 -0
  516. package/lib/types/host/services/schedule-mode/routes.js +109 -0
  517. package/lib/types/host/services/schedule-mode/session-create.d.ts +27 -0
  518. package/lib/types/host/services/schedule-mode/session-create.d.ts.map +1 -0
  519. package/lib/types/host/services/schedule-mode/session-create.js +21 -0
  520. package/lib/types/host/services/schedule-mode/store.d.ts +36 -0
  521. package/lib/types/host/services/schedule-mode/store.d.ts.map +1 -0
  522. package/lib/types/host/services/schedule-mode/store.js +309 -0
  523. package/lib/types/host/services/schedule-mode/tools.d.ts +13 -0
  524. package/lib/types/host/services/schedule-mode/tools.d.ts.map +1 -0
  525. package/lib/types/host/services/schedule-mode/tools.js +132 -0
  526. package/lib/types/host/services/ssh/engine.d.ts +162 -0
  527. package/lib/types/host/services/ssh/engine.d.ts.map +1 -0
  528. package/lib/types/host/services/ssh/engine.js +699 -0
  529. package/lib/types/host/services/ssh/host-keys.d.ts +10 -0
  530. package/lib/types/host/services/ssh/host-keys.d.ts.map +1 -0
  531. package/lib/types/host/services/ssh/host-keys.js +108 -0
  532. package/lib/types/host/services/ssh/index.d.ts +38 -0
  533. package/lib/types/host/services/ssh/index.d.ts.map +1 -0
  534. package/lib/types/host/services/ssh/index.js +47 -0
  535. package/lib/types/host/services/ssh/routes.d.ts +29 -0
  536. package/lib/types/host/services/ssh/routes.d.ts.map +1 -0
  537. package/lib/types/host/services/ssh/routes.js +465 -0
  538. package/lib/types/host/services/ssh/store.d.ts +40 -0
  539. package/lib/types/host/services/ssh/store.d.ts.map +1 -0
  540. package/lib/types/host/services/ssh/store.js +330 -0
  541. package/lib/types/host/services/ssh/terminal-ws.d.ts +9 -0
  542. package/lib/types/host/services/ssh/terminal-ws.d.ts.map +1 -0
  543. package/lib/types/host/services/ssh/terminal-ws.js +142 -0
  544. package/lib/types/host/services/ssh/tools.d.ts +11 -0
  545. package/lib/types/host/services/ssh/tools.d.ts.map +1 -0
  546. package/lib/types/host/services/ssh/tools.js +270 -0
  547. package/lib/types/host/services/web-render/contracts.d.ts +93 -0
  548. package/lib/types/host/services/web-render/contracts.d.ts.map +1 -0
  549. package/lib/types/host/services/web-render/contracts.js +60 -0
  550. package/lib/types/host/services/web-render/index.d.ts +58 -0
  551. package/lib/types/host/services/web-render/index.d.ts.map +1 -0
  552. package/lib/types/host/services/web-render/index.js +90 -0
  553. package/lib/types/host/services/web-render/provider.d.ts +37 -0
  554. package/lib/types/host/services/web-render/provider.d.ts.map +1 -0
  555. package/lib/types/host/services/web-render/provider.js +274 -0
  556. package/lib/types/host/services/web-search-anthropic/contracts.d.ts +117 -0
  557. package/lib/types/host/services/web-search-anthropic/contracts.d.ts.map +1 -0
  558. package/lib/types/host/services/web-search-anthropic/contracts.js +90 -0
  559. package/lib/types/host/services/web-search-anthropic/index.d.ts +53 -0
  560. package/lib/types/host/services/web-search-anthropic/index.d.ts.map +1 -0
  561. package/lib/types/host/services/web-search-anthropic/index.js +123 -0
  562. package/lib/types/host/services/web-search-anthropic/provider.d.ts +101 -0
  563. package/lib/types/host/services/web-search-anthropic/provider.d.ts.map +1 -0
  564. package/lib/types/host/services/web-search-anthropic/provider.js +237 -0
  565. package/lib/types/host/services/web-search-kaon/contracts.d.ts +114 -0
  566. package/lib/types/host/services/web-search-kaon/contracts.d.ts.map +1 -0
  567. package/lib/types/host/services/web-search-kaon/contracts.js +73 -0
  568. package/lib/types/host/services/web-search-kaon/index.d.ts +50 -0
  569. package/lib/types/host/services/web-search-kaon/index.d.ts.map +1 -0
  570. package/lib/types/host/services/web-search-kaon/index.js +114 -0
  571. package/lib/types/host/services/web-search-kaon/provider.d.ts +111 -0
  572. package/lib/types/host/services/web-search-kaon/provider.d.ts.map +1 -0
  573. package/lib/types/host/services/web-search-kaon/provider.js +244 -0
  574. package/lib/types/host/services/worktree/git-runner.d.ts +82 -0
  575. package/lib/types/host/services/worktree/git-runner.d.ts.map +1 -0
  576. package/lib/types/host/services/worktree/git-runner.js +81 -0
  577. package/lib/types/host/services/worktree/git.d.ts +94 -0
  578. package/lib/types/host/services/worktree/git.d.ts.map +1 -0
  579. package/lib/types/host/services/worktree/git.js +153 -0
  580. package/lib/types/host/services/worktree/http.d.ts +12 -0
  581. package/lib/types/host/services/worktree/http.d.ts.map +1 -0
  582. package/lib/types/host/services/worktree/http.js +34 -0
  583. package/lib/types/host/services/worktree/index.d.ts +38 -0
  584. package/lib/types/host/services/worktree/index.d.ts.map +1 -0
  585. package/lib/types/host/services/worktree/index.js +78 -0
  586. package/lib/types/host/services/worktree/lane-tool.d.ts +22 -0
  587. package/lib/types/host/services/worktree/lane-tool.d.ts.map +1 -0
  588. package/lib/types/host/services/worktree/lane-tool.js +92 -0
  589. package/lib/types/host/services/worktree/manager.d.ts +410 -0
  590. package/lib/types/host/services/worktree/manager.d.ts.map +1 -0
  591. package/lib/types/host/services/worktree/manager.js +1485 -0
  592. package/lib/types/host/services/worktree/model-override.d.ts +20 -0
  593. package/lib/types/host/services/worktree/model-override.d.ts.map +1 -0
  594. package/lib/types/host/services/worktree/model-override.js +35 -0
  595. package/lib/types/host/services/worktree/routes.d.ts +50 -0
  596. package/lib/types/host/services/worktree/routes.d.ts.map +1 -0
  597. package/lib/types/host/services/worktree/routes.js +695 -0
  598. package/lib/types/host/services/worktree/run-command.d.ts +26 -0
  599. package/lib/types/host/services/worktree/run-command.d.ts.map +1 -0
  600. package/lib/types/host/services/worktree/run-command.js +36 -0
  601. package/lib/types/host/services/worktree/session-lane/manager.d.ts +109 -0
  602. package/lib/types/host/services/worktree/session-lane/manager.d.ts.map +1 -0
  603. package/lib/types/host/services/worktree/session-lane/manager.js +172 -0
  604. package/lib/types/host/services/worktree/workspace-model-routes.d.ts +21 -0
  605. package/lib/types/host/services/worktree/workspace-model-routes.d.ts.map +1 -0
  606. package/lib/types/host/services/worktree/workspace-model-routes.js +198 -0
  607. package/lib/types/host/services/worktree/workspace-model.d.ts +48 -0
  608. package/lib/types/host/services/worktree/workspace-model.d.ts.map +1 -0
  609. package/lib/types/host/services/worktree/workspace-model.js +113 -0
  610. package/lib/types/index.d.ts +47 -0
  611. package/lib/types/index.d.ts.map +1 -0
  612. package/lib/types/index.js +52 -0
  613. package/lib/types/invariant.d.ts +4 -0
  614. package/lib/types/invariant.d.ts.map +1 -0
  615. package/lib/types/invariant.js +3 -0
  616. package/package.json +153 -3
  617. package/presets/code-vibe/THIRD_PARTY_NOTICES.md +55 -0
  618. package/presets/code-vibe/skills/UPSTREAM.md +104 -0
  619. package/presets/code-vibe/skills/agent-experience/SKILL.md +15 -0
  620. package/presets/code-vibe/skills/agent-operation/SKILL.md +43 -0
  621. package/presets/code-vibe/skills/code-review/SKILL.md +45 -0
  622. package/presets/code-vibe/skills/dsh-ci-test-reliability/SKILL.md +133 -0
  623. package/presets/code-vibe/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md +60 -0
  624. package/presets/code-vibe/skills/dsh-find-simplifications/SKILL.md +69 -0
  625. package/presets/code-vibe/skills/dsh-find-simplifications/references/historical-patterns.md +45 -0
  626. package/presets/code-vibe/skills/dsh-merging-stacked-prs/SKILL.md +129 -0
  627. package/presets/code-vibe/skills/dsh-prose-standard/SKILL.md +83 -0
  628. package/presets/code-vibe/skills/dsh-prose-standard/references/examples.md +169 -0
  629. package/presets/code-vibe/skills/dsh-speed-up-perf/SKILL.md +94 -0
  630. package/presets/code-vibe/skills/dsh-trim-cot-leakage/SKILL.md +47 -0
  631. package/presets/code-vibe/skills/dsh-trim-cot-leakage/references/examples.md +275 -0
  632. package/presets/code-vibe/skills/dsh-trim-cot-leakage/references/recall-batteries.md +52 -0
  633. package/presets/code-vibe/skills/frontend-design/SKILL.md +60 -0
  634. package/presets/code-vibe/skills/frontend-design/references/critique-format.md +35 -0
  635. package/presets/code-vibe/skills/frontend-design/references/design-standards/LOAD-MAP.md +81 -0
  636. package/presets/code-vibe/skills/frontend-design/references/design-standards/README.md +53 -0
  637. package/presets/code-vibe/skills/frontend-design/references/design-standards/anchor.md +51 -0
  638. package/presets/code-vibe/skills/frontend-design/references/design-standards/audit/ai-tells.md +59 -0
  639. package/presets/code-vibe/skills/frontend-design/references/design-standards/audit/web-interface.md +14 -0
  640. package/presets/code-vibe/skills/frontend-design/references/design-standards/change-control.md +62 -0
  641. package/presets/code-vibe/skills/frontend-design/references/design-standards/copy.md +65 -0
  642. package/presets/code-vibe/skills/frontend-design/references/design-standards/craft-knobs.md +61 -0
  643. package/presets/code-vibe/skills/frontend-design/references/design-standards/debug-playbook.md +37 -0
  644. package/presets/code-vibe/skills/frontend-design/references/design-standards/exemplars/README.md +64 -0
  645. package/presets/code-vibe/skills/frontend-design/references/design-standards/plan-architecture.checklist.json +13 -0
  646. package/presets/code-vibe/skills/frontend-design/references/design-standards/product-judgment.md +113 -0
  647. package/presets/code-vibe/skills/frontend-design/references/design-standards/system-architecture.md +108 -0
  648. package/presets/code-vibe/skills/frontend-design/references/design-standards/tokens/README.md +26 -0
  649. package/presets/code-vibe/skills/frontend-design/references/design-standards/tokens/color-roles.md +49 -0
  650. package/presets/code-vibe/skills/frontend-design/references/design-standards/tokens/motion.md +51 -0
  651. package/presets/code-vibe/skills/frontend-design/references/design-standards/tokens/scale.md +89 -0
  652. package/presets/code-vibe/skills/frontend-design/references/design-standards/ui-page-gate.md +61 -0
  653. package/presets/code-vibe/skills/frontend-design/references/design-standards/ui-surface.checklist.json +49 -0
  654. package/presets/code-vibe/skills/frontend-design/references/design-standards/ux.md +76 -0
  655. package/presets/code-vibe/skills/frontend-design/references/design-standards/visual.md +52 -0
  656. package/presets/code-vibe/skills/frontend-design/references/perceptible-path.md +27 -0
  657. package/presets/code-vibe/skills/frontend-design/references/product-package.md +80 -0
  658. package/presets/code-vibe/skills/frontend-design/references/scheme-antipatterns.md +21 -0
  659. package/presets/code-vibe/skills/quality-review/SKILL.md +57 -0
  660. package/presets/code-vibe/skills/quality-review/references/bounded-verify.md +37 -0
  661. package/presets/code-vibe/skills/quality-review/references/browser-verify.md +81 -0
  662. package/presets/code-vibe/skills/quality-review/references/evidence-contract.md +194 -0
  663. package/presets/code-vibe/skills/quality-review/references/falsify-checklist.md +44 -0
  664. package/presets/code-vibe/skills/quality-review/references/human-acceptance-pack.md +68 -0
  665. package/presets/code-vibe/skills/quality-review/references/product-regression.md +51 -0
  666. package/presets/code-vibe/skills/quality-review/references/ux-standards.md +37 -0
  667. package/presets/code-vibe/skills/quality-review/references/ux-walkthrough.md +49 -0
  668. package/presets/code-vibe/skills/quality-review/references/validation-report.md +22 -0
  669. package/presets/code-vibe/skills/quality-review/references/verification-loop.md +95 -0
  670. package/presets/code-vibe/skills/quality-review/references/version-acceptance-matrix.md +131 -0
  671. package/presets/code-vibe/skills/record-browser-gif/SKILL.md +174 -0
  672. package/presets/code-vibe/skills/record-browser-gif/scripts/encode_gif.py +337 -0
  673. package/presets/code-vibe/skills/record-browser-gif/scripts/test_encode_gif.py +103 -0
  674. package/presets/code-vibe/skills/wish-programming/SKILL.md +150 -0
  675. package/presets/code-vibe/skills/wish-programming/references/roles/arch-reviewer.md +26 -0
  676. package/presets/code-vibe/skills/wish-programming/references/roles/design-reviewer.md +32 -0
  677. package/presets/code-vibe/skills/wish-programming/references/roles/pm-reviewer.md +26 -0
  678. package/presets/code-vibe/skills/wish-programming/references/roles/qa-reviewer.md +16 -0
  679. package/presets/code-vibe/skills/wish-programming/references/roles/security-reviewer.md +16 -0
  680. package/presets/code-vibe/skills/wish-programming/references/roles/sre-reviewer.md +16 -0
  681. package/presets/code-vibe/skills/wish-programming/references/roles/ux-adversary.md +16 -0
  682. package/presets/code-vibe/skills/wish-programming/references/templates/arch.md.tpl +51 -0
  683. package/presets/code-vibe/skills/wish-programming/references/templates/brief.md.tpl +36 -0
  684. package/presets/code-vibe/skills/wish-programming/references/templates/confirmation-pack.md +54 -0
  685. package/presets/code-vibe/skills/wish-programming/references/templates/design-spec.md.tpl +49 -0
  686. package/presets/code-vibe/skills/wish-programming/references/templates/module-readme.md +37 -0
  687. package/presets/code-vibe/skills/wish-programming/references/templates/product.md.tpl +74 -0
  688. package/presets/code-vibe/skills/wish-programming/references/templates/requirements.md.tpl +31 -0
  689. package/presets/code-vibe/skills/wish-programming/references/workflow-template.md +122 -0
  690. package/presets/code-vibe/skills/wish-programming/scripts/MANIFEST.sha256 +11 -0
  691. package/presets/code-vibe/skills/wish-programming/scripts/check_spec.py +416 -0
  692. package/presets/code-vibe/skills/wish-programming/scripts/check_whitelist.py +193 -0
  693. package/presets/code-vibe/skills/wish-programming/scripts/coverage_check.py +199 -0
  694. package/presets/code-vibe/skills/wish-programming/scripts/gate-lock.sh +46 -0
  695. package/presets/code-vibe/skills/wish-programming/scripts/journey.sh +281 -0
  696. package/presets/code-vibe/skills/wish-programming/scripts/project.sh +152 -0
  697. package/presets/code-vibe/skills/wish-programming/scripts/replay.sh +65 -0
  698. package/presets/code-vibe/skills/wish-programming/scripts/scan_drift.py +79 -0
  699. package/presets/code-vibe/skills/wish-programming/scripts/verify.sh +169 -0
  700. package/presets/code-vibe/skills/wish-programming/scripts/verify_preset_budgets.py +218 -0
  701. package/presets/code-vibe/skills/wish-programming/scripts/wish.sh +150 -0
  702. package/presets/code-vibe.patch.yml +325 -0
  703. package/presets/hiopen.patch.yml +109 -0
  704. package/src/client/compat.ts +86 -0
  705. package/src/client/conversation/conversation.module.css +264 -0
  706. package/src/client/conversation/git-branch.tsx +152 -0
  707. package/src/client/conversation/git-graph/LICENSE.git-graph +202 -0
  708. package/src/client/conversation/git-graph/api.ts +85 -0
  709. package/src/client/conversation/git-graph/branch-chip.tsx +354 -0
  710. package/src/client/conversation/git-graph/branch-popover.tsx +185 -0
  711. package/src/client/conversation/git-graph/chip.tsx +47 -0
  712. package/src/client/conversation/git-graph/create-branch-dialog.tsx +108 -0
  713. package/src/client/conversation/git-graph/error-copy.ts +50 -0
  714. package/src/client/conversation/git-graph/git-graph.module.css +792 -0
  715. package/src/client/conversation/git-graph/graph-dialog.tsx +196 -0
  716. package/src/client/conversation/git-graph/hero-seat.ts +76 -0
  717. package/src/client/conversation/git-graph/locales.ts +90 -0
  718. package/src/client/conversation/git-graph/sse-leader.ts +124 -0
  719. package/src/client/conversation/index.ts +82 -0
  720. package/src/client/conversation/selector-context.tsx +24 -0
  721. package/src/client/conversation/session-lane-chip.tsx +262 -0
  722. package/src/client/conversation/session-lane-locales.ts +34 -0
  723. package/src/client/conversation/subagent-model-dom.ts +157 -0
  724. package/src/client/conversation/subagent-model.ts +198 -0
  725. package/src/client/conversation/worktree/api.ts +283 -0
  726. package/src/client/conversation/worktree/dialogs/DeleteWorktreeDialog.tsx +111 -0
  727. package/src/client/conversation/worktree/dialogs/PrCreateDialog.tsx +114 -0
  728. package/src/client/conversation/worktree/lane-binding.ts +77 -0
  729. package/src/client/conversation/worktree/lane-menu.tsx +247 -0
  730. package/src/client/conversation/worktree/lane-open.ts +204 -0
  731. package/src/client/conversation/worktree/lane-workspace-sweeper.ts +136 -0
  732. package/src/client/conversation/worktree/lifecycle.ts +75 -0
  733. package/src/client/conversation/worktree/locales.ts +97 -0
  734. package/src/client/conversation/worktree/semantic.ts +10 -0
  735. package/src/client/conversation/worktree/worktree.module.css +70 -0
  736. package/src/client/css-modules.d.ts +8 -0
  737. package/src/client/index.ts +58 -0
  738. package/src/client/lark-inbox/i18n.ts +125 -0
  739. package/src/client/lark-inbox/lark-wizard.module.css +88 -0
  740. package/src/client/lark-inbox/wizard.tsx +211 -0
  741. package/src/client/notify/i18n.ts +93 -0
  742. package/src/client/settings/family-scope.ts +45 -0
  743. package/src/client/settings/lark-inbox.tsx +319 -0
  744. package/src/client/settings/notify.tsx +288 -0
  745. package/src/client/settings/primitives-0.2.ts +148 -0
  746. package/src/client/settings/settings-shell.module.css +56 -0
  747. package/src/client/sidebar/RenderBoundary.tsx +17 -0
  748. package/src/client/sidebar/agent-browser.tsx +114 -0
  749. package/src/client/sidebar/diff-view.tsx +314 -0
  750. package/src/client/sidebar/git-workbench.tsx +700 -0
  751. package/src/client/sidebar/icons.tsx +23 -0
  752. package/src/client/sidebar/index.ts +28 -0
  753. package/src/client/sidebar/native-panels.tsx +149 -0
  754. package/src/client/sidebar/page-shell.module.css +96 -0
  755. package/src/client/sidebar/page-shell.tsx +86 -0
  756. package/src/client/sidebar/panels/memoir/api.ts +300 -0
  757. package/src/client/sidebar/panels/memoir/controller.ts +47 -0
  758. package/src/client/sidebar/panels/memoir/cwd.ts +140 -0
  759. package/src/client/sidebar/panels/memoir/i18n.ts +300 -0
  760. package/src/client/sidebar/panels/memoir/panel-activation.ts +42 -0
  761. package/src/client/sidebar/panels/memoir/panel.tsx +869 -0
  762. package/src/client/sidebar/panels/memoir/styles.ts +776 -0
  763. package/src/client/sidebar/panels/memoir/types.ts +7 -0
  764. package/src/client/sidebar/panels/ssh/ClusterTab.tsx +123 -0
  765. package/src/client/sidebar/panels/ssh/HostFormDialog.tsx +243 -0
  766. package/src/client/sidebar/panels/ssh/HostsTab.tsx +290 -0
  767. package/src/client/sidebar/panels/ssh/SshPanel.tsx +76 -0
  768. package/src/client/sidebar/panels/ssh/TerminalTab.tsx +290 -0
  769. package/src/client/sidebar/panels/ssh/TransferTab.tsx +232 -0
  770. package/src/client/sidebar/panels/ssh/TunnelsTab.tsx +204 -0
  771. package/src/client/sidebar/panels/ssh/api.ts +388 -0
  772. package/src/client/sidebar/panels/ssh/controller.ts +48 -0
  773. package/src/client/sidebar/panels/ssh/helpers.ts +70 -0
  774. package/src/client/sidebar/panels/ssh/locales.ts +318 -0
  775. package/src/client/sidebar/panels/ssh/panel.module.css +801 -0
  776. package/src/client/sidebar/panels/ssh/xterm.css.ts +2 -0
  777. package/src/client/sidebar/right-pane-i18n.ts +243 -0
  778. package/src/client/sidebar/right-pane.tsx +156 -0
  779. package/src/client/sidebar/scope.ts +54 -0
  780. package/src/client/sidebar/sidebar-header.module.css +72 -0
  781. package/src/client/sidebar/sidebar.module.css +619 -0
  782. package/src/client/trust.ts +187 -0
  783. package/src/client/ui-icons.ts +35 -0
  784. package/src/core/git-graph/commands.ts +107 -0
  785. package/src/core/git-graph/protocol.ts +238 -0
  786. package/src/core/index.ts +2 -0
  787. package/src/core/lark-inbox/gating.ts +35 -0
  788. package/src/core/lark-inbox/queue.ts +100 -0
  789. package/src/core/memoir/protocol.ts +46 -0
  790. package/src/core/no-browser-auth/contract.ts +25 -0
  791. package/src/core/notify/contract.ts +236 -0
  792. package/src/core/notify/feed.ts +235 -0
  793. package/src/core/playwright-sessions/contracts.ts +143 -0
  794. package/src/core/playwright-sessions/control.ts +33 -0
  795. package/src/core/playwright-sessions/index.ts +3 -0
  796. package/src/core/playwright-sessions/names.ts +83 -0
  797. package/src/core/playwright-sessions/schema.ts +102 -0
  798. package/src/core/playwright-sessions/wire.ts +142 -0
  799. package/src/core/remote-access/paths.ts +23 -0
  800. package/src/core/remote-web-ui/security.ts +171 -0
  801. package/src/core/right-pane/protocol.ts +94 -0
  802. package/src/core/schedule-mode/index.ts +6 -0
  803. package/src/core/schedule-mode/native.ts +105 -0
  804. package/src/core/schedule-mode/types.ts +182 -0
  805. package/src/core/schedule-mode/view.ts +66 -0
  806. package/src/core/ssh/protocol.ts +212 -0
  807. package/src/core/subagent-model/badges.ts +161 -0
  808. package/src/core/worktree/invariant.ts +101 -0
  809. package/src/core/worktree/manifest.ts +135 -0
  810. package/src/core/worktree/paths.ts +123 -0
  811. package/src/core/worktree/pr-create.ts +157 -0
  812. package/src/core/worktree/protocol.ts +7 -0
  813. package/src/core/worktree/types.ts +317 -0
  814. package/src/core/worktree/workspace-model.ts +61 -0
  815. package/src/host/services/access-trust/index.ts +127 -0
  816. package/src/host/services/access-trust/trust.ts +189 -0
  817. package/src/host/services/codex-bridge/bridge.ts +392 -0
  818. package/src/host/services/codex-bridge/codex-auth.ts +352 -0
  819. package/src/host/services/codex-bridge/dsh-integration.ts +169 -0
  820. package/src/host/services/codex-bridge/http.ts +105 -0
  821. package/src/host/services/codex-bridge/index.ts +154 -0
  822. package/src/host/services/codex-bridge/loopback.ts +63 -0
  823. package/src/host/services/codex-bridge/routes.ts +143 -0
  824. package/src/host/services/family-config.ts +224 -0
  825. package/src/host/services/family-namespaces.ts +39 -0
  826. package/src/host/services/git-graph/index.ts +11 -0
  827. package/src/host/services/git-graph/routes.ts +212 -0
  828. package/src/host/services/git-graph/service.ts +305 -0
  829. package/src/host/services/index.ts +94 -0
  830. package/src/host/services/lark-inbox/config.ts +66 -0
  831. package/src/host/services/lark-inbox/gateway.ts +184 -0
  832. package/src/host/services/lark-inbox/host-routes.ts +113 -0
  833. package/src/host/services/lark-inbox/inbox-store.ts +160 -0
  834. package/src/host/services/lark-inbox/index.ts +141 -0
  835. package/src/host/services/lark-inbox/tools.ts +124 -0
  836. package/src/host/services/lark-inbox/wizard.ts +206 -0
  837. package/src/host/services/lifecycle.ts +107 -0
  838. package/src/host/services/memoir/index.ts +80 -0
  839. package/src/host/services/memoir/routes.ts +346 -0
  840. package/src/host/services/memoir/settings.ts +122 -0
  841. package/src/host/services/memoir/store.ts +132 -0
  842. package/src/host/services/memoir/tools.ts +158 -0
  843. package/src/host/services/no-browser-auth/index.ts +70 -0
  844. package/src/host/services/notify/config.ts +178 -0
  845. package/src/host/services/notify/feishu-sender.ts +220 -0
  846. package/src/host/services/notify/host-routes.ts +133 -0
  847. package/src/host/services/notify/index.ts +305 -0
  848. package/src/host/services/playwright-sessions/child.ts +251 -0
  849. package/src/host/services/playwright-sessions/controller.ts +102 -0
  850. package/src/host/services/playwright-sessions/index.ts +146 -0
  851. package/src/host/services/playwright-sessions/live-view.ts +225 -0
  852. package/src/host/services/playwright-sessions/manager.ts +239 -0
  853. package/src/host/services/playwright-sessions/profile-gc.ts +97 -0
  854. package/src/host/services/playwright-sessions/routes.ts +90 -0
  855. package/src/host/services/remote-access/boot-scripts.ts +118 -0
  856. package/src/host/services/remote-access/index.ts +62 -0
  857. package/src/host/services/remote-access/routes.ts +137 -0
  858. package/src/host/services/remote-web-ui/index.ts +344 -0
  859. package/src/host/services/remote-web-ui/server-mode/accounts.ts +423 -0
  860. package/src/host/services/remote-web-ui/server-mode/cookie.ts +57 -0
  861. package/src/host/services/remote-web-ui/server-mode/dsh-home.ts +42 -0
  862. package/src/host/services/remote-web-ui/server-mode/external-auth.ts +296 -0
  863. package/src/host/services/remote-web-ui/server-mode/fence.ts +27 -0
  864. package/src/host/services/remote-web-ui/server-mode/gateway-routes.ts +797 -0
  865. package/src/host/services/remote-web-ui/server-mode/gateway.ts +684 -0
  866. package/src/host/services/remote-web-ui/server-mode/http.ts +105 -0
  867. package/src/host/services/remote-web-ui/server-mode/integrated-auth.ts +118 -0
  868. package/src/host/services/remote-web-ui/server-mode/lan.ts +19 -0
  869. package/src/host/services/remote-web-ui/server-mode/loopback.ts +63 -0
  870. package/src/host/services/remote-web-ui/server-mode/posture.ts +154 -0
  871. package/src/host/services/remote-web-ui/server-mode/roles.ts +51 -0
  872. package/src/host/services/remote-web-ui/server-mode/server-boot.ts +38 -0
  873. package/src/host/services/remote-web-ui/server-mode/sessions.ts +347 -0
  874. package/src/host/services/remote-web-ui/server.ts +591 -0
  875. package/src/host/services/resources.ts +78 -0
  876. package/src/host/services/right-pane/browser.ts +106 -0
  877. package/src/host/services/right-pane/index.ts +117 -0
  878. package/src/host/services/right-pane/wire.ts +10 -0
  879. package/src/host/services/schedule-mode/config.ts +26 -0
  880. package/src/host/services/schedule-mode/engine.ts +689 -0
  881. package/src/host/services/schedule-mode/index.ts +214 -0
  882. package/src/host/services/schedule-mode/routes.ts +130 -0
  883. package/src/host/services/schedule-mode/session-create.ts +36 -0
  884. package/src/host/services/schedule-mode/store.ts +304 -0
  885. package/src/host/services/schedule-mode/tools.ts +154 -0
  886. package/src/host/services/ssh/engine.ts +675 -0
  887. package/src/host/services/ssh/host-keys.ts +99 -0
  888. package/src/host/services/ssh/index.ts +84 -0
  889. package/src/host/services/ssh/routes.ts +442 -0
  890. package/src/host/services/ssh/store.ts +310 -0
  891. package/src/host/services/ssh/terminal-ws.ts +157 -0
  892. package/src/host/services/ssh/tools.ts +279 -0
  893. package/src/host/services/web-render/contracts.ts +138 -0
  894. package/src/host/services/web-render/index.ts +112 -0
  895. package/src/host/services/web-render/provider.ts +282 -0
  896. package/src/host/services/web-search-anthropic/contracts.ts +163 -0
  897. package/src/host/services/web-search-anthropic/index.ts +152 -0
  898. package/src/host/services/web-search-anthropic/provider.ts +325 -0
  899. package/src/host/services/worktree/git-runner.ts +124 -0
  900. package/src/host/services/worktree/git.ts +183 -0
  901. package/src/host/services/worktree/http.ts +36 -0
  902. package/src/host/services/worktree/index.ts +92 -0
  903. package/src/host/services/worktree/lane-tool.ts +100 -0
  904. package/src/host/services/worktree/manager.ts +1553 -0
  905. package/src/host/services/worktree/model-override.ts +43 -0
  906. package/src/host/services/worktree/routes.ts +570 -0
  907. package/src/host/services/worktree/run-command.ts +66 -0
  908. package/src/host/services/worktree/session-lane/manager.ts +228 -0
  909. package/src/host/services/worktree/workspace-model-routes.ts +156 -0
  910. package/src/host/services/worktree/workspace-model.ts +132 -0
  911. package/src/index.ts +84 -0
  912. package/src/invariant.ts +4 -0
@@ -0,0 +1,1553 @@
1
+ /**
2
+ * Host worktree manager: owns the per-repo worktree list, the on-disk
3
+ * manifest under `<repo>/.dsh/workspaces/`, and the workspace-registry
4
+ * bookkeeping. All mutations go through this class so the HTTP layer only
5
+ * knows how to translate envelopes and the browser half only ever sees the
6
+ * stable `WorktreeProject` / `WorktreeView` shapes from `core/types.ts`.
7
+ *
8
+ * Persistence model
9
+ * - Each repo has its own manifest at `<repo>/.dsh/workspaces/manifest.json`
10
+ * - We never write the manifest from the browser; the host owns the file
11
+ * - The manifest records every worktree we ever created; a missing
12
+ * directory is the "orphan" state, not a hard error
13
+ *
14
+ * Concurrency
15
+ * - Per-repo lock prevents two `POST /worktree/create` calls from racing
16
+ * on the same repo (one would otherwise see the other's worktree
17
+ * directory and 409)
18
+ * - The lock is in-process only; out-of-process git contention is the
19
+ * user's responsibility (we do not lock across processes)
20
+ *
21
+ * Data flow
22
+ * - The host returns a worktree-only index (`WorktreeIndex`) on
23
+ * `GET /worktree/tree`. Session data lives in the official client
24
+ * service `ctx.sessions.list` and is never carried through the HTTP
25
+ * layer; the browser merges both sides.
26
+ * @module dsh-client-ui-worktree/host/manager
27
+ */
28
+
29
+ import { existsSync, realpathSync } from 'node:fs'
30
+ import { mkdir, readFile } from 'node:fs/promises'
31
+ import { dirname, resolve, sep } from 'node:path'
32
+ import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
33
+ import type { Context } from '@deepseek-ai/cordis'
34
+ import type {} from '@deepseek-ai/dsh-subprocess'
35
+ import type {} from '@deepseek-ai/dsh-workspace'
36
+ import type { SessionId } from '@deepseek-ai/dsh-session/types'
37
+ import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
38
+ import { subprocessRunner as sharedSubprocessRunner, type GitRunner, type GitRunResult } from './git-runner.ts'
39
+ import { WorktreeError, defaultMessage, isValidBranchName } from '../../../core/worktree/invariant.ts'
40
+ import {
41
+ type CleanupWorktreeInput, type CreateLaneInput, type CreateWorktreeInput, type InitWorktreeInput,
42
+ type LaneHandle, type LaneLookupInput, type LaneView,
43
+ type PrCreateInput, type PrCreateResult, type RemoveWorktreeInput,
44
+ type StatusInput, type WorktreeIndex, type WorktreeProject, type WorktreeStatusIndex,
45
+ type WorktreeStatusView, type WorktreeView,
46
+ } from '../../../core/worktree/types.ts'
47
+ import {
48
+ emptyManifest, parseManifest, type ParsedWorktreeManifest, type WorktreeManifestEntry, manifestPath,
49
+ } from '../../../core/worktree/manifest.ts'
50
+ import { WORKSPACE_DIR, githubCompareUrl, githubOwnerRepo, isInsidePath, samePath, worktreeDir, worktreeRoot } from '../../../core/worktree/paths.ts'
51
+ import {
52
+ branchDeleteArgv, BRANCH_LIST_ARGV, checkIgnoreArgv, FETCH_PRUNE_ARGV, gitSpawnArgv, lsRemoteBranchArgv,
53
+ mergeBaseIsAncestorArgv, parseBranchList, pushUpstreamArgv, REMOTE_HEAD_ARGV, REMOTE_LIST_ARGV,
54
+ remoteGetUrlArgv, verifyRefArgv, verifyRemoteRefArgv, worktreeAddArgv, worktreeListPorcelain,
55
+ worktreeRemoveArgv, WORKTREE_PRUNE_ARGV,
56
+ } from './git.ts'
57
+
58
+ /** Deadline for a git command that talks to the network (fetch / ls-remote / ancestor probe). */
59
+ const GIT_NETWORK_TIMEOUT_MS = 15_000
60
+ /** Deadline for a push; a large branch on a slow link still has to finish. */
61
+ const GIT_PUSH_TIMEOUT_MS = 180_000
62
+ /**
63
+ * How long a status probe may be reused. The panel asks for status whenever it
64
+ * opens and after every mutation, and one probe costs a `git fetch` plus an
65
+ * `ls-remote`/ancestor check per lane. So the answer is cached, every write this
66
+ * manager performs invalidates it, and the TTL only bounds what a user changes
67
+ * outside the plugin (a commit, or a push from a terminal).
68
+ */
69
+ const STATUS_CACHE_TTL_MS = 60_000
70
+ /**
71
+ * How long the archive-driven lane retirement waits for the durable archive write
72
+ * to land, and how many times it re-checks. The `workspace/session-activity`
73
+ * waterfall runs BEFORE that write and cannot see its outcome.
74
+ */
75
+ const LANE_RETIRE_POLL_MS = 200
76
+ const LANE_RETIRE_ATTEMPTS = 5
77
+
78
+ /** Optional knobs for the worktree manager (mainly test seams). */
79
+ export interface WorktreeManagerOptions {
80
+ /** GitHub token env-var reference (credential-ref) for PR creation via the REST API. */
81
+ tokenEnv?: string
82
+ /** Runner override for tests; defaults to the shared subprocess runner. */
83
+ runner?: GitRunner
84
+ /** Deadlines (ms) for the network-touching git calls; a test seam narrows them. */
85
+ timeouts?: { networkMs?: number; pushMs?: number }
86
+ /** Status cache lifetime in ms (0 disables the cache). */
87
+ statusTtlMs?: number
88
+ }
89
+
90
+ /** Structural workspace shape used by cleanup (registry returns a richer entity). */
91
+ interface WorkspaceLike {
92
+ id: string
93
+ path: string
94
+ sessionIds?: readonly string[]
95
+ }
96
+
97
+ /** Public surface used by `routes.ts`. */
98
+ export class WorktreeManager {
99
+ private readonly runner: GitRunner
100
+ private readonly locks = new Map<string, Promise<void>>()
101
+ /** Last status answer per repo root, invalidated by every manifest write. */
102
+ private readonly statusCache = new Map<string, { index: WorktreeStatusIndex; at: number }>()
103
+
104
+ /** Required services: the subprocess seam and the workspace registry. */
105
+ constructor(private readonly ctx: Context, private readonly options: WorktreeManagerOptions = {}) {
106
+ this.runner = options.runner ?? sharedSubprocessRunner(ctx, { spawnArgv: (argv) => gitSpawnArgv(process.platform, argv) })
107
+ }
108
+
109
+ /** Status cache lifetime for this manager (0 disables it). */
110
+ private get statusTtlMs(): number {
111
+ const configured = this.options.statusTtlMs
112
+ return typeof configured === 'number' && configured >= 0 ? configured : STATUS_CACHE_TTL_MS
113
+ }
114
+
115
+ /** Network deadline for this manager (the options seam may narrow it). */
116
+ private get networkTimeoutMs(): number {
117
+ const configured = this.options.timeouts?.networkMs
118
+ return typeof configured === 'number' && configured > 0 ? configured : GIT_NETWORK_TIMEOUT_MS
119
+ }
120
+
121
+ /** Push deadline for this manager (the options seam may narrow it). */
122
+ private get pushTimeoutMs(): number {
123
+ const configured = this.options.timeouts?.pushMs
124
+ return typeof configured === 'number' && configured > 0 ? configured : GIT_PUSH_TIMEOUT_MS
125
+ }
126
+
127
+ /**
128
+ * Return the host-side worktree index: every registered workspace that
129
+ * lives under a git repo, plus the main worktree branch + dirty count
130
+ * and every linked worktree under `<repo>/.dsh/workspaces/`.
131
+ */
132
+ async getIndex(): Promise<WorktreeIndex> {
133
+ const registry = this.ctx.workspaceRegistry
134
+ const workspaces = registry.list()
135
+ const projects: WorktreeProject[] = []
136
+ const seen = new Set<string>()
137
+ for (const ws of workspaces) {
138
+ const root = await this.repoRoot(ws.path)
139
+ if (root === null || seen.has(root)) continue
140
+ seen.add(root)
141
+ const project = await this.buildProject(root)
142
+ if (project === null) continue
143
+ projects.push(project)
144
+ }
145
+ return {
146
+ projects: projects.sort((a, b) => a.name.localeCompare(b.name)),
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Create a linked worktree that cuts a NEW branch (`input.branch`) at
152
+ * `input.base`, and register it as a DSH workspace. Registration is
153
+ * best-effort here (a failure only surfaces as `workspaceId: null`);
154
+ * `createForSession` is the variant that requires it.
155
+ */
156
+ async create(input: CreateWorktreeInput): Promise<WorktreeView> {
157
+ if (!isValidBranchName(input.branch)) {
158
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { branch: input.branch })
159
+ }
160
+ const root = await this.repoRoot(input.repoPath)
161
+ if (root === null) {
162
+ throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
163
+ }
164
+ return this.withLock(root, async () => (await this.createLocked(root, input)).view)
165
+ }
166
+
167
+ /**
168
+ * Ensure the worktree behind one branch exists, and that its directory is a
169
+ * DSH workspace.
170
+ *
171
+ * This is the single creation path for a session-bound lane, and it merges
172
+ * what used to be two separate actions:
173
+ * - the branch already exists locally -> ATTACH it (git checks that
174
+ * branch out in its own directory; no new branch is invented);
175
+ * - the branch does not exist yet -> CUT it at `base` with `-b`.
176
+ * Either way the caller gets a directory that is that branch's checkout.
177
+ *
178
+ * Idempotent: an existing lane directory is returned as-is, so "switch to
179
+ * this branch" is safe to repeat and two sessions can share a branch.
180
+ *
181
+ * The workspace registration is not decoration and not ownership: the
182
+ * platform accounts a session to a workspace only while its cwd equals that
183
+ * workspace's path (`WorkspaceEntity.sessionIds`), and a blank conversation
184
+ * that no workspace accounts for renders an inert composer. Teardown
185
+ * unregisters the workspace with the directory, so the sidebar only carries
186
+ * lanes that are still alive.
187
+ */
188
+ async ensureLane(input: CreateLaneInput): Promise<LaneHandle> {
189
+ if (!isValidBranchName(input.branch)) {
190
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { branch: input.branch })
191
+ }
192
+ const root = await this.repoRoot(input.repoPath)
193
+ if (root === null) {
194
+ throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
195
+ }
196
+ const owner = root
197
+ return this.withLock(owner, async () => {
198
+ await this.ensureIgnored(owner)
199
+ await this.ensureWorkspaceDir(owner)
200
+ const dir = worktreeDir(owner, input.branch)
201
+ const existing = await this.worktreeBranchAt(owner, dir)
202
+ if (existing !== undefined) {
203
+ // The lane is already on disk: make sure it is reachable as a workspace
204
+ // too, because that is what lets a blank conversation move onto it.
205
+ const workspaceId = await this.ensureLaneWorkspace(owner, input.branch, dir)
206
+ return { ...existing, created: false, workspaceId }
207
+ }
208
+ const attach = await this.branchExists(owner, input.branch)
209
+ const argv = attach
210
+ // Existing local branch: check THAT branch out in the new directory.
211
+ ? worktreeAddArgv(dir, input.branch, false)
212
+ // New branch: `-b` is what makes the lane a usable work line. Without
213
+ // it git checks out the base commit-ish itself, which for anything
214
+ // that is not a branch name leaves a DETACHED HEAD — unpushable
215
+ // (`push -u origin <branch>` needs a local branch) and unmatched by
216
+ // `git worktree list --porcelain`.
217
+ : worktreeAddArgv(dir, input.base ?? 'HEAD', false, input.branch)
218
+ const result = await this.runner.run(argv, owner)
219
+ if (result.exitCode !== 0) {
220
+ throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
221
+ argv: [...argv], stderr: result.stderr,
222
+ })
223
+ }
224
+ const head = (await this.runner.run(['rev-parse', 'HEAD'], dir)).stdout.trim()
225
+ await this.appendManifestEntry(owner, {
226
+ name: input.branch,
227
+ path: dir,
228
+ baseCommit: head,
229
+ createdAt: new Date().toISOString(),
230
+ detached: false,
231
+ })
232
+ return {
233
+ name: input.branch,
234
+ branch: input.branch,
235
+ path: dir,
236
+ head: head === '' ? null : await this.headShort(dir),
237
+ created: true,
238
+ // The lane's own DSH workspace. The platform accounts a session to a
239
+ // workspace only while its cwd equals that workspace's path, and a blank
240
+ // conversation no workspace accounts for renders its composer inert, so
241
+ // the lane directory has to BE a workspace for the conversation in it to
242
+ // be usable at all. Teardown removes it again.
243
+ workspaceId: await this.ensureLaneWorkspace(owner, input.branch, dir),
244
+ }
245
+ })
246
+ }
247
+
248
+ /**
249
+ * The lane's DSH workspace, registered once and reused afterwards.
250
+ *
251
+ * Required, not a taste: `WorkspaceEntity.sessionIds` only accounts sessions
252
+ * whose cwd equals the workspace path, `attachSession` refuses any other cwd,
253
+ * and a blank conversation that no workspace accounts for shows an inert
254
+ * composer ("choose a workspace to start"). Registration failure is not fatal
255
+ * here — the worktree is real, so the caller reports the lane without a
256
+ * workspace and the browser declines to claim the conversation moved.
257
+ */
258
+ private async ensureLaneWorkspace(root: string, branch: string, dir: string): Promise<WorkspaceId | null> {
259
+ const registered = await this.workspaceForPath(dir)
260
+ if (registered !== null) return registered.id as WorkspaceId
261
+ try {
262
+ const created = await this.ctx.workspaceRegistry.create(dir, `${basename(root)} / ${branch}`)
263
+ await this.updateManifestEntry(root, branch, { workspaceId: created.id as WorkspaceId })
264
+ return created.id as WorkspaceId
265
+ } catch (error) {
266
+ console.warn('[dsh-worktree] lane workspace registration failed:', error)
267
+ return null
268
+ }
269
+ }
270
+
271
+ /** The lane already present at `dir`, when git reports one there. The
272
+ * workspace half is resolved by the caller (see ensureLane). */
273
+ private async worktreeBranchAt(root: string, dir: string): Promise<Omit<LaneHandle, 'created' | 'workspaceId'> | undefined> {
274
+ if (!existsSync(dir)) return undefined
275
+ const list = await this.runner.run(worktreeListPorcelain(), root)
276
+ if (list.exitCode !== 0) return undefined
277
+ const hit = parsePorcelainWorktrees(list.stdout).find((candidate) => resolve(candidate.path) === resolve(dir))
278
+ if (hit === undefined) return undefined
279
+ const branch = hit.branch ?? deriveNameFromPath(hit.path)
280
+ return {
281
+ name: branch,
282
+ branch,
283
+ path: hit.path,
284
+ head: await this.headShort(hit.path),
285
+ }
286
+ }
287
+
288
+ /** Shared create body; the caller holds the per-repo lock. */
289
+ private async createLocked(
290
+ root: string,
291
+ input: CreateWorktreeInput,
292
+ options: { requireWorkspace?: boolean } = {},
293
+ ): Promise<{ view: WorktreeView; workspaceId: WorkspaceId | null }> {
294
+ await this.ensureIgnored(root)
295
+ await this.ensureWorkspaceDir(root)
296
+ const dir = worktreeDir(root, input.branch)
297
+ if (existsSync(dir)) {
298
+ throw new WorktreeError('worktree-exists', defaultMessage('worktree-exists'), { dir })
299
+ }
300
+ if (await this.branchExists(root, input.branch)) {
301
+ throw new WorktreeError('worktree-exists', `A local branch named "${input.branch}" already exists; choose another branch name or delete that branch first.`, { branch: input.branch })
302
+ }
303
+ const base = input.base ?? 'HEAD'
304
+ // `-b` is what makes the lane a usable work line. Without it git checks
305
+ // out the base commit-ish itself, which for anything that is not a
306
+ // branch name leaves a DETACHED HEAD: `git push -u origin <branch>`
307
+ // then fails (no local branch) and `git worktree list --porcelain`
308
+ // reports `detached`, so the lane never matches its own name.
309
+ const argv = worktreeAddArgv(dir, base, false, input.branch)
310
+ const result = await this.runner.run(argv, root)
311
+ if (result.exitCode !== 0) {
312
+ throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
313
+ argv: [...argv], stderr: result.stderr,
314
+ })
315
+ }
316
+ const head = (await this.runner.run(['rev-parse', 'HEAD'], dir)).stdout.trim()
317
+ const entry: WorktreeManifestEntry = {
318
+ name: input.branch,
319
+ path: dir,
320
+ baseCommit: head,
321
+ createdAt: new Date().toISOString(),
322
+ detached: false,
323
+ }
324
+ await this.appendManifestEntry(root, entry)
325
+ let workspaceId: WorkspaceId | null = null
326
+ try {
327
+ const created = await this.ctx.workspaceRegistry.create(dir, `${basename(root)} / ${input.branch}`)
328
+ workspaceId = created.id
329
+ await this.updateManifestEntry(root, input.branch, { workspaceId })
330
+ } catch (error) {
331
+ if (options.requireWorkspace === true) {
332
+ await this.rollbackLocked(root, input.branch)
333
+ throw new WorktreeError(
334
+ 'git-failed',
335
+ `workspace registration failed for lane "${input.branch}": ${String(error)}`,
336
+ { dir },
337
+ )
338
+ }
339
+ console.warn('[dsh-worktree] workspace registration failed:', error)
340
+ }
341
+ return {
342
+ view: {
343
+ name: input.branch,
344
+ path: dir,
345
+ branch: input.branch,
346
+ head: head === '' ? null : await this.headShort(dir),
347
+ exists: true,
348
+ workspaceId,
349
+ sessionId: null,
350
+ },
351
+ workspaceId,
352
+ }
353
+ }
354
+
355
+ /**
356
+ * The manifest record for one lane: its directory when the repository's
357
+ * manifest records that branch, null otherwise.
358
+ *
359
+ * The bind path reads this instead of trusting `laneForPath`, which
360
+ * synthesizes a lane view for a directory the manifest does not know about —
361
+ * so "the lane exists on disk" and "the host recorded this lane" are two
362
+ * different answers, and only the second may authorize a binding.
363
+ */
364
+ async laneEntry(repoPath: string, name: string): Promise<{ root: string; path: string } | null> {
365
+ if (!isValidBranchName(name)) {
366
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name })
367
+ }
368
+ const root = await this.repoRoot(repoPath)
369
+ if (root === null) return null
370
+ const manifest = await this.readManifest(root)
371
+ const entry = manifest.worktrees.find((candidate) => candidate.name === name)
372
+ return entry === undefined ? null : { root, path: entry.path }
373
+ }
374
+
375
+ /**
376
+ * Record the session bound to a lane. Called right after the session exists,
377
+ * so the header chip and the teardown guards can resolve `sessionId -> lane`
378
+ * without re-deriving it from the filesystem.
379
+ * @param repoPath - repository root.
380
+ * @param name - worktree (branch) name.
381
+ * @param sessionId - the session to record.
382
+ * @param permissionPresetApplied - whether the lane's access preset landed.
383
+ */
384
+ async bindSession(
385
+ repoPath: string,
386
+ name: string,
387
+ sessionId: SessionId,
388
+ permissionPresetApplied: boolean,
389
+ ): Promise<void> {
390
+ if (!isValidBranchName(name)) {
391
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name })
392
+ }
393
+ const root = await this.repoRoot(repoPath)
394
+ if (root === null) {
395
+ throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: repoPath })
396
+ }
397
+ await this.withLock(root, async () => {
398
+ await this.updateManifestEntry(root, name, { sessionId, permissionPresetApplied })
399
+ })
400
+ }
401
+
402
+ /**
403
+ * Undo a lane creation. Used when the lane exists but its session could
404
+ * not be created, so a failed create never leaves half a lane behind:
405
+ * worktree directory, local branch, admin records, manifest entry and the
406
+ * registered workspace all go away.
407
+ */
408
+ async rollbackCreate(input: { repoPath: string; name: string }): Promise<void> {
409
+ if (!isValidBranchName(input.name)) {
410
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name: input.name })
411
+ }
412
+ const root = await this.repoRoot(input.repoPath)
413
+ if (root === null) {
414
+ throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
415
+ }
416
+ await this.withLock(root, async () => {
417
+ // Rollback exists for a lane whose session never came up. A lane that has
418
+ // a live session under it is not that case: deleting the directory and the
419
+ // branch out from under a running conversation is exactly what remove and
420
+ // cleanup refuse to do, so rollback must not be the way around them.
421
+ const dir = worktreeDir(root, input.name)
422
+ const workspace = await this.workspaceForPath(dir)
423
+ const busySession = await this.liveSessionOnLane(root, dir, workspace)
424
+ if (busySession !== undefined) {
425
+ throw new WorktreeError('worktree-busy', defaultMessage('worktree-busy'), { sessionId: busySession })
426
+ }
427
+ await this.rollbackLocked(root, input.name)
428
+ })
429
+ }
430
+
431
+ /**
432
+ * Resolve the lane a path (or a session's cwd) belongs to. Returns null
433
+ * for the main checkout and for any path outside `<repo>/.dsh/workspaces/`.
434
+ *
435
+ * Deliberately network-free: this feeds the conversation header chip, so
436
+ * it must stay cheap. `merged` is therefore only the locally knowable
437
+ * half (branch pushed AND its tip is an ancestor of the base); the
438
+ * "remote branch deleted on merge" signal needs a `fetch --prune` and
439
+ * stays with the panel's `POST /worktree/status` probe.
440
+ */
441
+ async laneForPath(input: LaneLookupInput): Promise<LaneView | null> {
442
+ // 1. Cwd-based: the session is already inside a lane directory.
443
+ const target = input.path ?? (input.sessionId === undefined ? undefined : this.sessionCwd(input.sessionId))
444
+ if (target !== undefined && target !== '') {
445
+ const root = await this.repoRoot(target)
446
+ if (root !== null) {
447
+ const laneRoot = resolve(worktreeRoot(root))
448
+ const normalized = resolve(target)
449
+ if (normalized !== laneRoot && normalized.startsWith(laneRoot + sep)) {
450
+ const fromPath = await this.laneForDirectory(root, normalized)
451
+ if (fromPath !== null) return fromPath
452
+ }
453
+ }
454
+ }
455
+ // 2. Binding-based: the conversation carries a lane recorded on the
456
+ // manifest even though its cwd is still the main checkout (an
457
+ // existing conversation cannot move). Without this a mid-conversation
458
+ // branch switch would be invisible to the header chip and the lane tool.
459
+ if (input.sessionId !== undefined) {
460
+ const cwd = this.sessionCwd(input.sessionId)
461
+ if (cwd !== undefined) {
462
+ const root = await this.repoRoot(cwd)
463
+ if (root !== null) {
464
+ const entry = await this.manifestEntryForSession(root, input.sessionId)
465
+ if (entry !== undefined) return this.laneFromEntry(root, entry)
466
+ }
467
+ }
468
+ }
469
+ return null
470
+ }
471
+
472
+ /** One lane record for a directory git reports as a linked worktree. */
473
+ private async laneForDirectory(root: string, dir: string): Promise<LaneView | null> {
474
+ const listResult = await this.runner.run(worktreeListPorcelain(), root)
475
+ if (listResult.exitCode !== 0) return null
476
+ const wt = parsePorcelainWorktrees(listResult.stdout).find((candidate) => resolve(candidate.path) === dir)
477
+ if (wt === undefined) return null
478
+ const manifest = await this.readManifest(root)
479
+ const name = wt.branch ?? deriveNameFromPath(wt.path)
480
+ const entry = manifest.worktrees.find((e) => e.path === wt.path || e.name === name)
481
+ return this.laneFromEntry(root, entry ?? { name, path: wt.path } as WorktreeManifestEntry, wt.branch, dir)
482
+ }
483
+
484
+ /** One lane record from a manifest entry, with the git state filled in. */
485
+ private async laneFromEntry(
486
+ root: string,
487
+ entry: WorktreeManifestEntry,
488
+ branchHint?: string | null,
489
+ pathHint?: string,
490
+ ): Promise<LaneView> {
491
+ const path = pathHint ?? entry.path
492
+ const name = entry.name
493
+ const branch = branchHint ?? name
494
+ const base = await this.remoteDefaultBranch(root) ?? 'main'
495
+ // The lane view answers the same question the cleanup guard does, through
496
+ // the same rule. Deriving "merged" here from `pushed` alone made the header
497
+ // chip say "not pushed" for every branch GitHub deleted on merge — while
498
+ // cleanup, which uses worktreeStatus, considered it merged and tore it down.
499
+ const status = await this.worktreeStatus(root, name, base, entry)
500
+ return {
501
+ name,
502
+ branch,
503
+ path,
504
+ repoPath: root,
505
+ exists: existsSync(path),
506
+ workspaceId: (entry.workspaceId as WorkspaceId | undefined) ?? null,
507
+ sessionId: (entry.sessionId as SessionId | undefined) ?? null,
508
+ permissionPresetApplied: entry.permissionPresetApplied ?? null,
509
+ dirty: await this.dirtyCount(path),
510
+ pushed: status.pushed,
511
+ remoteBranchGone: status.remoteBranchGone,
512
+ merged: status.merged,
513
+ prUrl: entry.prUrl ?? null,
514
+ prNumber: entry.prNumber ?? null,
515
+ }
516
+ }
517
+
518
+ /** The manifest entry a session is bound to, within one repo. */
519
+ private async manifestEntryForSession(root: string, sessionId: string): Promise<WorktreeManifestEntry | undefined> {
520
+ const manifest = await this.readManifest(root)
521
+ return manifest.worktrees.find((entry) => entry.sessionId === sessionId)
522
+ }
523
+
524
+ /**
525
+ * Archive the session a lane was bound to, when one is recorded. Once the
526
+ * lane directory is gone the conversation would otherwise keep pointing at
527
+ * a deleted path; archiving hides the record instead of letting it dangle.
528
+ * Best-effort — the workspace registry may be unavailable.
529
+ *
530
+ * DSH 0.2 refuses to archive a session that reports active work (a running
531
+ * turn, a job, a subagent, an active schedule) unless `stopActivity` is
532
+ * requested; the lane's own session is exactly the one this teardown is
533
+ * allowed to take down, so the archive must write first and then ask the
534
+ * activity owners to stop.
535
+ */
536
+ private async archiveBoundSession(root: string, name: string): Promise<void> {
537
+ const manifest = await this.readManifest(root)
538
+ const sessionId = manifest.worktrees.find((entry) => entry.name === name)?.sessionId
539
+ if (sessionId === undefined) return
540
+ try {
541
+ await this.ctx.workspaceRegistry.archiveSession(sessionId as SessionId, { stopActivity: true })
542
+ } catch (error) {
543
+ console.warn(`[dsh-worktree] archive session ${sessionId} failed:`, error)
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Refuse to create a lane when the repository does not ignore the lane
549
+ * root. Without the ignore rule the lane directory shows up as untracked
550
+ * in the main checkout, and a `git add -A` there would sweep an entire
551
+ * lane into the trunk — exactly the trunk pollution that lanes exist to
552
+ * prevent.
553
+ */
554
+ private async ensureIgnored(root: string): Promise<void> {
555
+ // Probe a child path so a directory rule (`/.dsh/workspaces/`) matches.
556
+ const probe = `${worktreeRoot(root)}/probe`
557
+ const result = await this.runner.run(checkIgnoreArgv(probe), root)
558
+ if (result.exitCode !== 0) {
559
+ throw new WorktreeError(
560
+ 'worktree-not-ignored',
561
+ `${defaultMessage('worktree-not-ignored')} Add "${WORKSPACE_DIR}/" to ${root}/.gitignore.`,
562
+ {
563
+ dir: worktreeRoot(root),
564
+ hint: `add "${WORKSPACE_DIR}/" to ${root}/.gitignore`,
565
+ },
566
+ )
567
+ }
568
+ }
569
+
570
+ /** Whether a local branch of that name already exists. */
571
+ private async branchExists(root: string, branch: string): Promise<boolean> {
572
+ const result = await this.runner.run(verifyRefArgv(branch), root)
573
+ return result.exitCode === 0
574
+ }
575
+
576
+ /** The cwd recorded on a live session header, when the host can see it. */
577
+ private sessionCwd(sessionId: string): string | undefined {
578
+ const sessions = optionalService<{ get?: (id: string) => { header?: { cwd?: string } } | undefined }>(this.ctx, 'sessions')
579
+ const cwd = sessions?.get?.(sessionId)?.header?.cwd
580
+ return typeof cwd === 'string' && cwd !== '' ? cwd : undefined
581
+ }
582
+
583
+ /** Patch one manifest entry in place; a missing entry is a no-op. */
584
+ private async updateManifestEntry(
585
+ repoPath: string,
586
+ name: string,
587
+ patch: Partial<WorktreeManifestEntry>,
588
+ ): Promise<void> {
589
+ const manifest = await this.readManifest(repoPath)
590
+ let touched = false
591
+ manifest.worktrees = manifest.worktrees.map((entry) => {
592
+ if (entry.name !== name) return entry
593
+ touched = true
594
+ return { ...entry, ...patch }
595
+ })
596
+ if (touched) await this.writeManifest(repoPath, manifest)
597
+ }
598
+
599
+ /** Best-effort teardown of a lane that never became usable. */
600
+ private async rollbackLocked(root: string, name: string): Promise<void> {
601
+ const dir = worktreeDir(root, name)
602
+ if (existsSync(dir)) {
603
+ await this.runner.run(worktreeRemoveArgv(dir, true), root)
604
+ }
605
+ if (await this.branchExists(root, name)) {
606
+ await this.runner.run(branchDeleteArgv(name, true), root)
607
+ }
608
+ await this.runner.run(WORKTREE_PRUNE_ARGV, root)
609
+ await this.removeManifestEntry(root, name)
610
+ const workspace = await this.workspaceForPath(dir)
611
+ if (workspace !== null) {
612
+ try {
613
+ await this.ctx.workspaceRegistry.delete(workspace.id as WorkspaceId)
614
+ } catch (error) {
615
+ console.warn('[dsh-worktree] rollback workspace unregister failed:', error)
616
+ }
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Remove a worktree without silently discarding user state. A live session
622
+ * always blocks removal; a dirty checkout requires the caller's explicit
623
+ * force confirmation. Cleanup remains the preferred merged-aware flow.
624
+ */
625
+ async remove(input: RemoveWorktreeInput): Promise<{ name: string; path: string }> {
626
+ if (!isValidBranchName(input.name)) {
627
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name: input.name })
628
+ }
629
+ const root = await this.repoRoot(input.repoPath)
630
+ if (root === null) {
631
+ throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
632
+ }
633
+ return this.withLock(root, async () => {
634
+ const dir = worktreeDir(root, input.name)
635
+ if (!existsSync(dir)) {
636
+ throw new WorktreeError('worktree-not-found', defaultMessage('worktree-not-found'), { dir })
637
+ }
638
+ const workspace = await this.workspaceForPath(dir)
639
+ const busySession = await this.liveSessionOnLane(root, dir, workspace, input.sessionId)
640
+ if (busySession !== undefined) {
641
+ throw new WorktreeError('worktree-busy', defaultMessage('worktree-busy'), { sessionId: busySession })
642
+ }
643
+ const dirty = await this.runner.run(['status', '--porcelain'], dir)
644
+ if (dirty.exitCode !== 0) {
645
+ throw new WorktreeError('git-failed', defaultMessage('git-failed'), { stderr: dirty.stderr })
646
+ }
647
+ if (dirty.stdout.trim() !== '' && input.force !== true) {
648
+ throw new WorktreeError('worktree-busy', 'The worktree has uncommitted changes; confirm force removal to discard them.', { dir, dirty: true })
649
+ }
650
+ const argv = worktreeRemoveArgv(dir, input.force === true)
651
+ const result = await this.runner.run(argv, root)
652
+ if (result.exitCode !== 0) {
653
+ throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
654
+ argv: [...argv], stderr: result.stderr,
655
+ })
656
+ }
657
+ await this.runner.run(WORKTREE_PRUNE_ARGV, root)
658
+ await this.archiveBoundSession(root, input.name)
659
+ await this.removeManifestEntry(root, input.name)
660
+ // Unregister the workspace resolved BEFORE the directory went away: a
661
+ // lookup by a deleted path finds nothing, and the sidebar would keep a
662
+ // stale row for a directory that is gone (and offer to start a session in
663
+ // it). Cleanup already resolved it up front for the same reason.
664
+ if (workspace !== null) {
665
+ try {
666
+ await this.ctx.workspaceRegistry.delete(workspace.id as WorkspaceId)
667
+ } catch (error) {
668
+ console.warn('[dsh-worktree] workspace unregister failed:', error)
669
+ }
670
+ }
671
+ return { name: input.name, path: dir }
672
+ })
673
+ }
674
+
675
+ /**
676
+ * `git init -b <initialBranch>` on a directory that is not yet a git
677
+ * repository; idempotent (a no-op if the directory is already a repo).
678
+ * The host-side caller is the browser dialog that asks "convert this
679
+ * workspace into a git project so I can use worktrees on it".
680
+ */
681
+ async init(input: InitWorktreeInput): Promise<{ repoPath: string; initialBranch: string }> {
682
+ const path = resolve(input.repoPath)
683
+ if (!existsSync(path)) {
684
+ throw new WorktreeError('workspace-unknown', defaultMessage('workspace-unknown'), { path })
685
+ }
686
+ const existing = await this.repoRoot(path)
687
+ if (existing !== null) {
688
+ return { repoPath: existing, initialBranch: input.initialBranch ?? 'main' }
689
+ }
690
+ // Converting a directory into a repository mutates that directory, so it is
691
+ // limited to a workspace the user actually registered with the host. Without
692
+ // this the route is a `git init` anywhere on the machine.
693
+ const workspace = await this.workspaceForPath(path)
694
+ if (workspace === null || !samePath(workspace.path, path)) {
695
+ throw new WorktreeError('workspace-unknown', defaultMessage('workspace-unknown'), { path })
696
+ }
697
+ const branch = input.initialBranch ?? 'main'
698
+ if (!isValidBranchName(branch)) {
699
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { branch })
700
+ }
701
+ const argv = gitSpawnArgv(process.platform, ['init', '-b', branch])
702
+ const result = await this.runner.run(argv, path)
703
+ if (result.exitCode !== 0) {
704
+ throw new WorktreeError('git-failed', defaultMessage('git-failed'), { stderr: result.stderr })
705
+ }
706
+ await this.ensureWorkspaceDir(path)
707
+ return { repoPath: path, initialBranch: branch }
708
+ }
709
+
710
+ // ─── PR lifecycle: create / status / cleanup ─────────────────────────────
711
+
712
+ /**
713
+ * Push the worktree branch and open (or create) a pull request. Always
714
+ * pushes to `origin`; a PR page is then produced either through the
715
+ * GitHub compare URL (default, no credentials) or through the GitHub
716
+ * REST API when a `tokenEnv` credential is configured. The resulting
717
+ * URL and metadata are recorded in the manifest so the panel can resume
718
+ * the PR later.
719
+ */
720
+ async prCreate(input: PrCreateInput): Promise<PrCreateResult> {
721
+ if (!isValidBranchName(input.name)) {
722
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name: input.name })
723
+ }
724
+ const root = await this.repoRoot(input.repoPath)
725
+ if (root === null) {
726
+ throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
727
+ }
728
+ const dir = worktreeDir(root, input.name)
729
+ if (!existsSync(dir)) {
730
+ throw new WorktreeError('worktree-not-found', defaultMessage('worktree-not-found'), { dir })
731
+ }
732
+ const remoteUrl = await this.originRemoteUrl(root)
733
+ if (remoteUrl === null) {
734
+ throw new WorktreeError('no-remote', defaultMessage('no-remote'), { repoPath: root })
735
+ }
736
+ return this.withLock(root, async () => {
737
+ // Push the branch with upstream tracking, from the worktree itself.
738
+ const push = await this.runBounded(pushUpstreamArgv(input.name), dir, this.pushTimeoutMs)
739
+ if (push.exitCode !== 0) {
740
+ throw new WorktreeError('push-failed', defaultMessage('push-failed'), {
741
+ branch: input.name, stderr: push.stderr,
742
+ })
743
+ }
744
+ const manifest = await this.readManifest(root)
745
+ const baseRef = input.baseRef ?? manifest.baseBranch ?? await this.remoteDefaultBranch(root) ?? 'main'
746
+ const title = input.title?.trim() !== '' && input.title !== undefined ? input.title!.trim() : input.name
747
+ // Preferred: REST API PR creation when a token is configured.
748
+ const viaApi = await this.createPullRequestViaApi(root, remoteUrl, input.name, baseRef, title)
749
+ let prUrl: string | null
750
+ let prNumber: number | undefined
751
+ let method: PrCreateResult['method']
752
+ if (viaApi !== null) {
753
+ prUrl = viaApi.htmlUrl
754
+ prNumber = viaApi.number
755
+ method = 'api'
756
+ } else {
757
+ prUrl = githubCompareUrl(remoteUrl, baseRef, input.name)
758
+ if (prUrl === null) {
759
+ throw new WorktreeError('unsupported-remote', defaultMessage('unsupported-remote'), { remoteUrl })
760
+ }
761
+ method = 'url'
762
+ }
763
+ const entry = manifest.worktrees.find((e) => e.name === input.name)
764
+ if (entry === undefined) {
765
+ throw new WorktreeError('worktree-not-found', defaultMessage('worktree-not-found'), { name: input.name })
766
+ }
767
+ entry.prUrl = prUrl
768
+ if (prNumber !== undefined) entry.prNumber = prNumber
769
+ entry.remoteBranch = 'origin'
770
+ entry.baseRef = baseRef
771
+ entry.updatedAt = new Date().toISOString()
772
+ if (manifest.baseBranch === undefined) manifest.baseBranch = baseRef
773
+ await this.writeManifest(root, manifest)
774
+ return { prUrl, method }
775
+ })
776
+ }
777
+
778
+ /**
779
+ * PR / merge status for one repo (or one named worktree of it): whether
780
+ * the branch was pushed, whether the remote branch is gone, and whether
781
+ * it is merged into the base. Runs a best-effort `git fetch origin
782
+ * --prune` first so a merged-and-deleted remote branch reads as merged.
783
+ *
784
+ * A full-repo answer is cached: this endpoint is polled whenever the panel
785
+ * opens, while every mutation this manager performs invalidates the entry, so
786
+ * a poll is a memory read in the normal flow. `force` (the explicit refresh
787
+ * action) and a name-filtered probe always go to git.
788
+ */
789
+ async status(input: StatusInput): Promise<WorktreeStatusIndex> {
790
+ const root = await this.repoRoot(input.repoPath)
791
+ if (root === null) {
792
+ throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
793
+ }
794
+ if (input.name !== undefined) return await this.probeStatus(root, input.name)
795
+ if (input.force !== true) {
796
+ const cached = this.cachedStatus(root)
797
+ if (cached !== undefined) return cached
798
+ }
799
+ const index = await this.probeStatus(root, undefined)
800
+ this.statusCache.set(root, { index, at: Date.now() })
801
+ return index
802
+ }
803
+
804
+ /** The cached status for a repo, or undefined when absent or past its TTL. */
805
+ private cachedStatus(root: string): WorktreeStatusIndex | undefined {
806
+ const ttl = this.statusTtlMs
807
+ if (ttl <= 0) return undefined
808
+ const hit = this.statusCache.get(root)
809
+ if (hit === undefined) return undefined
810
+ if (Date.now() - hit.at >= ttl) {
811
+ this.statusCache.delete(root)
812
+ return undefined
813
+ }
814
+ return hit.index
815
+ }
816
+
817
+ /** Drop the cached status for a repo; every manifest write calls this. */
818
+ private invalidateStatus(root: string): void {
819
+ this.statusCache.delete(root)
820
+ }
821
+
822
+ /** One uncached probe of the repository. */
823
+ private async probeStatus(root: string, name: string | undefined): Promise<WorktreeStatusIndex> {
824
+ const baseBranch = await this.remoteDefaultBranch(root) ?? 'main'
825
+ const manifest = await this.readManifest(root)
826
+ const effectiveBase = manifest.baseBranch ?? baseBranch
827
+ await this.fetchPruneBestEffort(root)
828
+ const listResult = await this.runner.run(worktreeListPorcelain(), root)
829
+ const parsed = listResult.exitCode === 0 ? parsePorcelainWorktrees(listResult.stdout) : []
830
+ const worktrees: WorktreeStatusView[] = []
831
+ for (const wt of parsed) {
832
+ if (wt.path === root) continue
833
+ if (!wt.path.startsWith(worktreeRoot(root) + '/') && !wt.path.startsWith(worktreeRoot(root) + '\\')) continue
834
+ if (name !== undefined && wt.branch !== name) continue
835
+ const entry = manifest.worktrees.find((e) => e.path === wt.path)
836
+ const branch = wt.branch
837
+ if (branch === null) {
838
+ worktrees.push({
839
+ name: entry?.name ?? deriveNameFromPath(wt.path),
840
+ path: wt.path,
841
+ branch: null,
842
+ pushed: false,
843
+ remoteBranchGone: false,
844
+ merged: false,
845
+ prUrl: entry?.prUrl ?? null,
846
+ })
847
+ continue
848
+ }
849
+ const statusView = await this.worktreeStatus(root, branch, effectiveBase, entry)
850
+ worktrees.push({
851
+ name: entry?.name ?? deriveNameFromPath(wt.path),
852
+ path: wt.path,
853
+ branch,
854
+ ...statusView,
855
+ prUrl: entry?.prUrl ?? null,
856
+ })
857
+ }
858
+ const branchResult = await this.runner.run(BRANCH_LIST_ARGV, root)
859
+ const branches = branchResult.exitCode === 0 ? parseBranchList(branchResult.stdout) : []
860
+ return { baseBranch: effectiveBase, worktrees, branches }
861
+ }
862
+
863
+ /**
864
+ * Clean up a merged worktree: validate no sessions are running, verify
865
+ * the branch is merged (or the user forced), remove the worktree
866
+ * directory, delete the local branch, unregister the workspace, and
867
+ * archive its sessions. `force` abandons an unmerged branch and drops
868
+ * uncommitted changes.
869
+ */
870
+ async cleanup(input: CleanupWorktreeInput): Promise<{ name: string; path: string }> {
871
+ if (!isValidBranchName(input.name)) {
872
+ throw new WorktreeError('invalid-branch', defaultMessage('invalid-branch'), { name: input.name })
873
+ }
874
+ const root = await this.repoRoot(input.repoPath)
875
+ if (root === null) {
876
+ throw new WorktreeError('not-a-repository', defaultMessage('not-a-repository'), { path: input.repoPath })
877
+ }
878
+ const dir = worktreeDir(root, input.name)
879
+ const dirExists = existsSync(dir)
880
+ // A missing directory is an orphan: skip the worktree-remove step and
881
+ // still delete the branch / manifest entry / workspace below.
882
+ return this.withLock(root, async () => {
883
+ // Never clean up a worktree a live session is bound to — except the
884
+ // conversation that is itself asking (see liveSessionOnLane).
885
+ const workspace = await this.workspaceForPath(dir)
886
+ const busySession = await this.liveSessionOnLane(root, dir, workspace, input.sessionId)
887
+ if (busySession !== undefined) {
888
+ throw new WorktreeError('worktree-busy', defaultMessage('worktree-busy'), { sessionId: busySession })
889
+ }
890
+ const baseBranch = await this.remoteDefaultBranch(root) ?? 'main'
891
+ const manifest = await this.readManifest(root)
892
+ const effectiveBase = manifest.baseBranch ?? baseBranch
893
+ const entry = manifest.worktrees.find((e) => e.name === input.name)
894
+ // Deleting the local branch is this flow's job, so it requires the host's
895
+ // own record of the lane. Without it, any name that happens to be a local
896
+ // branch - a directory nobody recorded, or a branch with no lane at all -
897
+ // would be deleted here with nothing tying it to this plugin. The directory
898
+ // may be gone (an orphan lane) as long as the record survives.
899
+ if (entry === undefined) {
900
+ throw new WorktreeError('worktree-not-found', defaultMessage('worktree-not-found'), { name: input.name })
901
+ }
902
+ await this.fetchPruneBestEffort(root)
903
+ // Always probe. The merged decision must not depend on whether THIS
904
+ // plugin created the PR: a branch pushed from the lane terminal has to
905
+ // reach the same answer, or the panel offers a cleanup the host refuses.
906
+ const status = await this.worktreeStatus(root, input.name, effectiveBase, entry)
907
+ const merged = status.merged || input.force === true
908
+ if (!merged) {
909
+ throw new WorktreeError('not-merged', defaultMessage('not-merged'), { branch: input.name, base: effectiveBase })
910
+ }
911
+ if (dirExists) {
912
+ const remove = await this.runner.run(worktreeRemoveArgv(dir, input.force === true), root)
913
+ if (remove.exitCode !== 0) {
914
+ throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
915
+ argv: [...worktreeRemoveArgv(dir, input.force === true)], stderr: remove.stderr,
916
+ })
917
+ }
918
+ await this.runner.run(WORKTREE_PRUNE_ARGV, root)
919
+ }
920
+ // Delete the local branch only when the branch exists (detached worktrees skip).
921
+ const verify = await this.runner.run(['rev-parse', '--verify', '--quiet', `refs/heads/${input.name}`], root)
922
+ if (verify.exitCode === 0) {
923
+ const deleteArgv = branchDeleteArgv(input.name, input.force === true)
924
+ const deleted = await this.runner.run(deleteArgv, root)
925
+ if (deleted.exitCode !== 0) {
926
+ // `-d` also refuses a branch merged into the PR base but not into the
927
+ // main worktree's current HEAD. The merge state is verified above, so
928
+ // escalate instead of reporting success with the branch left behind.
929
+ const forced = input.force === true ? deleted : await this.runner.run(branchDeleteArgv(input.name, true), root)
930
+ if (forced.exitCode !== 0) {
931
+ throw new WorktreeError('git-failed', defaultMessage('git-failed'), {
932
+ argv: [...deleteArgv], stderr: forced.stderr,
933
+ })
934
+ }
935
+ }
936
+ }
937
+ await this.archiveBoundSession(root, input.name)
938
+ await this.removeManifestEntry(root, input.name)
939
+ if (workspace !== null) {
940
+ await this.archiveAndDeleteWorkspace(workspace)
941
+ }
942
+ return { name: input.name, path: dir }
943
+ })
944
+ }
945
+
946
+ // ─── lane workspace lifecycle ─────────────────────────────────────────────
947
+
948
+ /**
949
+ * Retire the workspace registration of a lane whose conversation was archived.
950
+ *
951
+ * A lane's directory must be a registered workspace while its conversation
952
+ * runs — the platform accounts a session to a workspace only when the session
953
+ * cwd equals that workspace's path, and a blank conversation no workspace
954
+ * accounts for renders an inert composer — so the sidebar row is unavoidable
955
+ * while the lane is alive. It does not have to outlive the conversation:
956
+ * archiving retires the registration, which keeps the worktree, the branch and
957
+ * the binding, so picking that branch again re-registers the directory in one
958
+ * call and nothing the user did is discarded.
959
+ *
960
+ * The `workspace/session-activity` waterfall runs BEFORE the durable archive
961
+ * write, so this waits for the archive set to show the session instead of
962
+ * assuming the write lands: an archive refused for running work retires
963
+ * nothing. A lane that still has an unarchived conversation keeps its
964
+ * workspace, or that conversation would lose the workspace it runs in.
965
+ * @param sessionId - the conversation that was archived.
966
+ * @param options - bounded-poll knobs (test seam).
967
+ * @returns whether a lane workspace was retired.
968
+ */
969
+ async retireArchivedLane(sessionId: string, options: { attempts?: number; pollMs?: number } = {}): Promise<boolean> {
970
+ const attempts = Math.max(1, options.attempts ?? LANE_RETIRE_ATTEMPTS)
971
+ const pollMs = options.pollMs ?? LANE_RETIRE_POLL_MS
972
+ for (let attempt = 1; ; attempt += 1) {
973
+ if (this.isArchivedSession(sessionId)) return await this.retireLaneWorkspaceOf(sessionId)
974
+ if (attempt >= attempts) return false
975
+ await new Promise((resolve) => { setTimeout(resolve, pollMs) })
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Retire every lane workspace whose recorded conversation is archived.
981
+ *
982
+ * Rows left behind by an earlier build, or by an archive that happened while
983
+ * this plugin was absent, must not survive a restart; this walks the registered
984
+ * workspaces once and drops exactly those lanes. Anything else is left alone: a
985
+ * workspace outside `<repo>/.dsh/workspaces/`, a lane whose manifest records no
986
+ * conversation, a lane whose recorded conversation is still live, and a lane
987
+ * that still has an unarchived conversation.
988
+ * @returns how many lane workspaces were retired.
989
+ */
990
+ async reconcileLaneWorkspaces(): Promise<number> {
991
+ let workspaces: readonly WorkspaceLike[]
992
+ try {
993
+ workspaces = this.ctx.workspaceRegistry.list() as readonly WorkspaceLike[]
994
+ } catch (error) {
995
+ console.warn('[dsh-worktree] lane workspace reconciliation skipped:', error)
996
+ return 0
997
+ }
998
+ let retired = 0
999
+ for (const workspace of workspaces) {
1000
+ if (typeof workspace.path !== 'string' || workspace.path === '') continue
1001
+ const root = await this.repoRoot(workspace.path)
1002
+ if (root === null) continue
1003
+ if (!isInsidePath(worktreeRoot(root), workspace.path)) continue
1004
+ const manifest = await this.readManifest(root)
1005
+ const entry = manifest.worktrees.find((candidate) => samePath(candidate.path, workspace.path))
1006
+ if (entry?.sessionId === undefined || !this.isArchivedSession(entry.sessionId)) continue
1007
+ if (!this.hasNoLiveConversation(workspace)) continue
1008
+ if (await this.retireLaneWorkspace(root, entry)) retired += 1
1009
+ }
1010
+ return retired
1011
+ }
1012
+
1013
+ // ─── private helpers ──────────────────────────────────────────────────────
1014
+
1015
+ /**
1016
+ * Run one git command under a deadline. An aborted command surfaces as
1017
+ * exitCode 124 with the reason on stderr, so every caller keeps its existing
1018
+ * exitCode check as the single failure path. Without this a stalled
1019
+ * `git fetch` held the per-repo lock forever and left the panel loading.
1020
+ */
1021
+ private async runBounded(argv: readonly string[], cwd: string, timeoutMs: number): Promise<GitRunResult> {
1022
+ try {
1023
+ return await this.runner.run(argv, cwd, AbortSignal.timeout(timeoutMs))
1024
+ } catch (error) {
1025
+ return {
1026
+ exitCode: 124,
1027
+ stdout: '',
1028
+ stderr: `git timed out after ${String(timeoutMs)}ms: ${error instanceof Error ? error.message : String(error)}`,
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ /** Acquire the per-repo lock for the duration of `fn`. */
1034
+ private async withLock<T>(repoRoot: string, fn: () => Promise<T>): Promise<T> {
1035
+ const previous = this.locks.get(repoRoot) ?? Promise.resolve()
1036
+ let release: (() => void) | undefined
1037
+ const next = new Promise<void>((resolve) => { release = resolve })
1038
+ this.locks.set(repoRoot, previous.then(() => next))
1039
+ await previous
1040
+ try {
1041
+ return await fn()
1042
+ } finally {
1043
+ release!()
1044
+ if (this.locks.get(repoRoot) === next) this.locks.delete(repoRoot)
1045
+ }
1046
+ }
1047
+
1048
+ /**
1049
+ * Resolve the canonical git common-dir for `path`, or null when `path`
1050
+ * is not inside a git working tree. We do this with `git rev-parse
1051
+ * --path-format=absolute --git-common-dir` so the result follows
1052
+ * `git`'s notion of canonicalization (worktrees resolve to the main
1053
+ * repo), not `realpath`'s.
1054
+ */
1055
+ private async repoRoot(path: string): Promise<string | null> {
1056
+ if (path === '' || !existsSync(path)) return null
1057
+ const result = await this.runner.run(
1058
+ ['rev-parse', '--path-format=absolute', '--git-common-dir'],
1059
+ path,
1060
+ )
1061
+ if (result.exitCode !== 0) return null
1062
+ const trimmed = result.stdout.trim()
1063
+ if (trimmed === '') return null
1064
+ return trimmed.endsWith('/.git') || trimmed.endsWith('\\.git')
1065
+ ? dirname(trimmed)
1066
+ : trimmed.replace(/[\\/]+$/, '')
1067
+ }
1068
+
1069
+ // ─── PR lifecycle helpers ────────────────────────────────────────────────
1070
+
1071
+ /** Resolve the `origin` remote URL, or null when no remote is configured. */
1072
+ private async originRemoteUrl(root: string): Promise<string | null> {
1073
+ const list = await this.runner.run(REMOTE_LIST_ARGV, root)
1074
+ if (list.exitCode !== 0) return null
1075
+ if (!list.stdout.split('\n').map((line) => line.trim()).includes('origin')) return null
1076
+ const url = await this.runner.run(remoteGetUrlArgv('origin'), root)
1077
+ if (url.exitCode !== 0) return null
1078
+ const trimmed = url.stdout.trim()
1079
+ return trimmed === '' ? null : trimmed
1080
+ }
1081
+
1082
+ /** Remote default branch (`refs/remotes/origin/HEAD`), or null when unset. */
1083
+ private async remoteDefaultBranch(root: string): Promise<string | null> {
1084
+ const result = await this.runner.run(REMOTE_HEAD_ARGV, root)
1085
+ if (result.exitCode !== 0) return null
1086
+ const ref = result.stdout.trim()
1087
+ if (ref === '') return null
1088
+ return ref.startsWith('refs/remotes/origin/') ? ref.slice('refs/remotes/origin/'.length) : ref
1089
+ }
1090
+
1091
+ /** Best-effort `git fetch origin --prune`; a failure (offline) is ignored. */
1092
+ private async fetchPruneBestEffort(root: string): Promise<void> {
1093
+ try {
1094
+ await this.runBounded(FETCH_PRUNE_ARGV, root, this.networkTimeoutMs)
1095
+ } catch {
1096
+ // offline / no network — keep stale refs and report from what we have
1097
+ }
1098
+ }
1099
+
1100
+ /** Whether `refs/remotes/origin/<branch>` exists after the last fetch. */
1101
+ private async remoteBranchPushed(root: string, branch: string): Promise<boolean> {
1102
+ const result = await this.runner.run(verifyRemoteRefArgv(branch), root)
1103
+ return result.exitCode === 0
1104
+ }
1105
+
1106
+ /** Whether the remote branch is gone (ls-remote --exit-code non-zero). */
1107
+ private async remoteBranchGone(root: string, branch: string): Promise<boolean> {
1108
+ const result = await this.runBounded(lsRemoteBranchArgv(branch), root, this.networkTimeoutMs)
1109
+ // A deadline is not evidence: an unanswered probe must not read as "gone",
1110
+ // because "gone" is a merged signal that lets a cleanup delete the lane.
1111
+ if (result.exitCode === 124) return false
1112
+ return result.exitCode !== 0
1113
+ }
1114
+
1115
+ /** Whether `origin/<branch>` is an ancestor of `origin/<base>` (or local `<base>`). */
1116
+ private async isAncestorOfBase(root: string, branch: string, base: string): Promise<boolean> {
1117
+ const originBase = await this.runner.run(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${base}`], root)
1118
+ const baseRef = originBase.exitCode === 0 ? `refs/remotes/origin/${base}` : base
1119
+ const result = await this.runBounded(mergeBaseIsAncestorArgv(`refs/remotes/origin/${branch}`, baseRef), root, this.networkTimeoutMs)
1120
+ return result.exitCode === 0
1121
+ }
1122
+
1123
+ /**
1124
+ * Whether the LOCAL branch tip is an ancestor of the base. Used when the
1125
+ * tracking ref is already pruned: the remote signal is gone, but the local
1126
+ * branch still answers whether its work landed in the base.
1127
+ */
1128
+ private async isLocalAncestorOfBase(root: string, branch: string, base: string): Promise<boolean> {
1129
+ const originBase = await this.runner.run(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${base}`], root)
1130
+ const baseRef = originBase.exitCode === 0 ? `refs/remotes/origin/${base}` : base
1131
+ const result = await this.runBounded(mergeBaseIsAncestorArgv(`refs/heads/${branch}`, baseRef), root, this.networkTimeoutMs)
1132
+ return result.exitCode === 0
1133
+ }
1134
+
1135
+ /**
1136
+ * Whether the local branch moved past the commit the lane was cut from. A
1137
+ * lane that never committed is an ancestor of its base too, so this is what
1138
+ * separates "merged work" from "no work yet".
1139
+ */
1140
+ private async branchTipMoved(root: string, branch: string, baseCommit: string): Promise<boolean> {
1141
+ if (baseCommit === '') return false
1142
+ const result = await this.runner.run(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], root)
1143
+ if (result.exitCode !== 0) return false
1144
+ const tip = result.stdout.trim()
1145
+ return tip !== '' && tip !== baseCommit
1146
+ }
1147
+
1148
+ /**
1149
+ * The merged signal for one branch, in two independent halves:
1150
+ *
1151
+ * 1. the remote branch still exists -> merged when its tip is an ancestor of
1152
+ * the base tip;
1153
+ * 2. the tracking ref is pruned -> either the branch was never pushed, or it
1154
+ * was deleted on merge ("delete branch on merge" is GitHub's default).
1155
+ * `remoteBranch` on the manifest only records a push this plugin made, so
1156
+ * a manually pushed branch would read as unmerged forever; the manifest's
1157
+ * `baseCommit` plus the local tip answer the same question without it.
1158
+ */
1159
+ private async worktreeStatus(
1160
+ root: string,
1161
+ branch: string,
1162
+ base: string,
1163
+ entry: WorktreeManifestEntry | undefined,
1164
+ ): Promise<{ pushed: boolean; remoteBranchGone: boolean; merged: boolean }> {
1165
+ const pushed = await this.remoteBranchPushed(root, branch)
1166
+ if (!pushed) {
1167
+ // tracking ref pruned: either never pushed, or pushed then deleted on merge
1168
+ const wasPushedBefore = entry?.remoteBranch === 'origin'
1169
+ const gone = wasPushedBefore && await this.remoteBranchGone(root, branch)
1170
+ const moved = entry?.baseCommit !== undefined && await this.branchTipMoved(root, branch, entry.baseCommit)
1171
+ const merged = gone || (moved && await this.isLocalAncestorOfBase(root, branch, base))
1172
+ return { pushed: false, remoteBranchGone: gone, merged }
1173
+ }
1174
+ const merged = await this.isAncestorOfBase(root, branch, base)
1175
+ return { pushed: true, remoteBranchGone: false, merged }
1176
+ }
1177
+
1178
+ /**
1179
+ * Create the PR through the GitHub REST API when a token is configured
1180
+ * and the remote is GitHub. Returns null to fall back to the compare URL
1181
+ * when no token is configured, the remote is not GitHub, or the API call
1182
+ * fails (the compare URL remains the zero-credential default).
1183
+ */
1184
+ private async createPullRequestViaApi(
1185
+ root: string,
1186
+ remoteUrl: string,
1187
+ branch: string,
1188
+ baseRef: string,
1189
+ title: string,
1190
+ ): Promise<{ htmlUrl: string; number: number } | null> {
1191
+ const parsed = githubOwnerRepo(remoteUrl)
1192
+ if (parsed === null) return null
1193
+ const tokenEnv = this.options.tokenEnv
1194
+ if (tokenEnv === undefined || tokenEnv === '') return null
1195
+ const credentials = (this.ctx as unknown as { credentials?: { resolve?: (ref: string) => Promise<{ value?: string } | undefined> } }).credentials
1196
+ if (credentials?.resolve === undefined) return null
1197
+ const credential = await credentials.resolve(tokenEnv)
1198
+ const token = credential?.value
1199
+ if (token === undefined || token === '') return null
1200
+ try {
1201
+ const response = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}/pulls`, {
1202
+ method: 'POST',
1203
+ headers: {
1204
+ authorization: `Bearer ${token}`,
1205
+ accept: 'application/vnd.github+json',
1206
+ 'content-type': 'application/json',
1207
+ },
1208
+ body: JSON.stringify({ title, head: branch, base: baseRef }),
1209
+ })
1210
+ if (!response.ok) {
1211
+ console.warn(`[dsh-worktree] GitHub PR create failed with status ${response.status}; falling back to compare URL`)
1212
+ return null
1213
+ }
1214
+ const payload = await response.json() as { html_url?: string; number?: number }
1215
+ if (typeof payload.html_url !== 'string' || typeof payload.number !== 'number') return null
1216
+ return { htmlUrl: payload.html_url, number: payload.number }
1217
+ } catch (error) {
1218
+ console.warn('[dsh-worktree] GitHub API unreachable; falling back to compare URL:', error)
1219
+ return null
1220
+ }
1221
+ }
1222
+
1223
+ /** Resolve the DSH workspace registered for a worktree directory, or null. */
1224
+ private async workspaceForPath(dir: string): Promise<WorkspaceLike | null> {
1225
+ try {
1226
+ const ws = await this.ctx.workspaceRegistry.resolveByPath(dir)
1227
+ return ws === undefined || ws === null ? null : ws
1228
+ } catch (error) {
1229
+ console.warn('[dsh-worktree] resolveByPath failed:', error)
1230
+ return null
1231
+ }
1232
+ }
1233
+
1234
+ /** Whether the registry's durable archive set holds this session. */
1235
+ private isArchivedSession(sessionId: string): boolean {
1236
+ try {
1237
+ const registry = this.ctx.workspaceRegistry as unknown as { archivedSessionIds?: readonly string[] }
1238
+ return (registry.archivedSessionIds ?? []).includes(sessionId)
1239
+ } catch (error) {
1240
+ console.warn('[dsh-worktree] could not read the workspace archive set:', error)
1241
+ return false
1242
+ }
1243
+ }
1244
+
1245
+ /** Whether every conversation a workspace accounts for has been archived. */
1246
+ private hasNoLiveConversation(workspace: WorkspaceLike): boolean {
1247
+ return this.sessionIdsOf(workspace).every((sessionId) => this.isArchivedSession(sessionId))
1248
+ }
1249
+
1250
+ /** Retire the workspace of the lane that records this conversation. */
1251
+ private async retireLaneWorkspaceOf(sessionId: string): Promise<boolean> {
1252
+ const cwd = this.sessionCwd(sessionId)
1253
+ if (cwd === undefined) return false
1254
+ const root = await this.repoRoot(cwd)
1255
+ if (root === null) return false
1256
+ const manifest = await this.readManifest(root)
1257
+ const entry = manifest.worktrees.find((candidate) => candidate.sessionId === sessionId)
1258
+ if (entry === undefined) return false
1259
+ return await this.retireLaneWorkspace(root, entry)
1260
+ }
1261
+
1262
+ /**
1263
+ * Drop one lane's workspace registration and the id recorded for it. The
1264
+ * directory, the worktree record, the branch and the session binding survive,
1265
+ * so picking that branch again re-registers the directory in one call.
1266
+ */
1267
+ private async retireLaneWorkspace(root: string, entry: WorktreeManifestEntry): Promise<boolean> {
1268
+ return this.withLock(root, async () => {
1269
+ const workspace = await this.workspaceForPath(entry.path)
1270
+ if (workspace === null) return false
1271
+ // A lane can host more than one conversation (two sessions on the same
1272
+ // branch). Retiring the workspace under a live one would take its composer
1273
+ // away, so the row stays until every conversation there is archived.
1274
+ if (!this.hasNoLiveConversation(workspace)) return false
1275
+ try {
1276
+ await this.ctx.workspaceRegistry.delete(workspace.id as WorkspaceId)
1277
+ } catch (error) {
1278
+ console.warn('[dsh-worktree] lane workspace retirement failed:', error)
1279
+ return false
1280
+ }
1281
+ await this.updateManifestEntry(root, entry.name, { workspaceId: undefined })
1282
+ return true
1283
+ })
1284
+ }
1285
+
1286
+ /** Session ids recorded on a workspace (structural — avoids registry type churn). */
1287
+ private sessionIdsOf(workspace: WorkspaceLike): readonly string[] {
1288
+ return workspace.sessionIds ?? []
1289
+ }
1290
+
1291
+ /** Whether a session id is currently live in the session store. */
1292
+ private isLiveSession(sessionId: string): boolean {
1293
+ const sessions = optionalService<{ get?: (id: string) => unknown }>(this.ctx, 'sessions')
1294
+ return sessions?.get?.(sessionId) !== undefined
1295
+ }
1296
+
1297
+ /**
1298
+ * The live session still using this lane, if any.
1299
+ *
1300
+ * Two sources: the lane's workspace accounts for the sessions whose cwd is that
1301
+ * directory, and the manifest entry records the session this plugin created
1302
+ * there. The manifest half is what stops `cleanup`/`remove` from deleting the
1303
+ * directory a running conversation is working in.
1304
+ *
1305
+ * `self` is the conversation asking for the teardown through its own lane
1306
+ * chip. It may excuse itself — otherwise the interlock would make a lane the
1307
+ * conversation sits on impossible to ever tear down (the chip is the only
1308
+ * surface left once the conversation is gone), and the teardown archives that
1309
+ * session anyway. The excusal is not a bypass: the lane has to actually
1310
+ * record the id (as its bound session, or as a session of its workspace), so
1311
+ * an unrecorded or foreign id excuses nothing.
1312
+ */
1313
+ private async liveSessionOnLane(
1314
+ root: string,
1315
+ dir: string,
1316
+ workspace: WorkspaceLike | null,
1317
+ self?: string,
1318
+ ): Promise<string | undefined> {
1319
+ const recorded = new Set<string>()
1320
+ if (workspace !== null) for (const sessionId of this.sessionIdsOf(workspace)) recorded.add(sessionId)
1321
+ const manifest = await this.readManifest(root)
1322
+ const entry = manifest.worktrees.find((candidate) => candidate.path === dir)
1323
+ if (entry?.sessionId !== undefined) recorded.add(entry.sessionId)
1324
+ const excused = self !== undefined && recorded.has(self) ? self : undefined
1325
+ for (const sessionId of recorded) {
1326
+ if (sessionId !== excused && this.isLiveSession(sessionId)) return sessionId
1327
+ }
1328
+ return undefined
1329
+ }
1330
+
1331
+ /** Archive every session of a worktree workspace, then unregister it. */
1332
+ private async archiveAndDeleteWorkspace(workspace: WorkspaceLike): Promise<void> {
1333
+ for (const sessionId of this.sessionIdsOf(workspace)) {
1334
+ try {
1335
+ // See archiveBoundSession: 0.2 archives write first, then stop the
1336
+ // session's running work, instead of refusing an active session.
1337
+ await this.ctx.workspaceRegistry.archiveSession(sessionId as SessionId, { stopActivity: true })
1338
+ } catch (error) {
1339
+ console.warn(`[dsh-worktree] archive session ${sessionId} failed:`, error)
1340
+ }
1341
+ }
1342
+ try {
1343
+ await this.ctx.workspaceRegistry.delete(workspace.id as WorkspaceId)
1344
+ } catch (error) {
1345
+ console.warn(`[dsh-worktree] workspace unregister failed for ${workspace.id}:`, error)
1346
+ }
1347
+ }
1348
+
1349
+ /**
1350
+ * Resolve the DSH workspace registered for a path. `resolveByPath` compares
1351
+ * the literal string, but git reports canonicalized paths (`/private/tmp/...`
1352
+ * on macOS while the registry holds `/tmp/...`), so a miss falls back to
1353
+ * comparing realpaths. Without this the main row's Open / New session buttons
1354
+ * stay disabled for every workspace behind a symlinked path.
1355
+ */
1356
+ private async workspaceIdFor(path: string): Promise<WorkspaceId | null> {
1357
+ try {
1358
+ const direct = await this.ctx.workspaceRegistry.resolveByPath(path)
1359
+ if (direct !== undefined && direct !== null) return direct.id
1360
+ } catch (error) {
1361
+ console.warn('[dsh-worktree] resolveByPath failed:', error)
1362
+ }
1363
+ const wanted = canonicalPath(path)
1364
+ if (wanted === null) return null
1365
+ try {
1366
+ const listed = this.ctx.workspaceRegistry.list() as readonly { id: string; path?: unknown }[]
1367
+ for (const workspace of listed) {
1368
+ if (typeof workspace.path !== 'string') continue
1369
+ if (canonicalPath(workspace.path) === wanted) return workspace.id as WorkspaceId
1370
+ }
1371
+ } catch (error) {
1372
+ console.warn('[dsh-worktree] workspace list failed:', error)
1373
+ }
1374
+ return null
1375
+ }
1376
+
1377
+ /**
1378
+ * Build the per-repo project view: main + linked worktrees under
1379
+ * `<repo>/.dsh/workspaces/`. The `git worktree list --porcelain`
1380
+ * output is parsed by hand because it is the only stable format and
1381
+ * we want zero dependencies.
1382
+ */
1383
+ private async buildProject(repoPath: string): Promise<WorktreeProject | null> {
1384
+ const listResult = await this.runner.run(worktreeListPorcelain(), repoPath)
1385
+ const parsed = listResult.exitCode === 0 ? parsePorcelainWorktrees(listResult.stdout) : []
1386
+ const [mainBranch, mainDirty] = await Promise.all([
1387
+ this.headBranch(repoPath),
1388
+ this.dirtyCount(repoPath),
1389
+ ])
1390
+ const manifest = await this.readManifest(repoPath)
1391
+ const worktrees: WorktreeView[] = []
1392
+ const mainWorkspaceId = await this.workspaceIdFor(repoPath)
1393
+ for (const wt of parsed) {
1394
+ if (wt.path === repoPath) continue
1395
+ // Only include worktrees that live under `<repo>/.dsh/workspaces/`.
1396
+ if (!wt.path.startsWith(worktreeRoot(repoPath) + '/') && !wt.path.startsWith(worktreeRoot(repoPath) + '\\')) continue
1397
+ const entry = manifest.worktrees.find((e) => e.path === wt.path || e.name === (wt.branch ?? deriveNameFromPath(wt.path)))
1398
+ const workspaceId = await this.workspaceIdFor(wt.path)
1399
+ worktrees.push({
1400
+ name: entry?.name ?? deriveNameFromPath(wt.path),
1401
+ path: wt.path,
1402
+ branch: wt.branch,
1403
+ head: await this.headShort(wt.path),
1404
+ exists: existsSync(wt.path),
1405
+ workspaceId,
1406
+ // The manifest is the only binding a session-bound lane has; it is what
1407
+ // lets the panel open the conversation instead of a (nonexistent) workspace.
1408
+ sessionId: (entry?.sessionId as SessionId | undefined) ?? null,
1409
+ })
1410
+ }
1411
+ return {
1412
+ repoPath,
1413
+ mainPath: repoPath,
1414
+ mainWorkspaceId,
1415
+ name: basename(repoPath),
1416
+ mainBranch,
1417
+ dirty: mainDirty,
1418
+ worktrees,
1419
+ }
1420
+ }
1421
+
1422
+ /** Read the manifest from disk; return an empty manifest when missing. */
1423
+ private async readManifest(repoPath: string): Promise<ParsedWorktreeManifest> { const file = manifestPath(repoPath)
1424
+ try {
1425
+ const text = await readFile(file, 'utf8')
1426
+ return parseManifest(text)
1427
+ } catch (error) {
1428
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return emptyManifest(repoPath)
1429
+ throw new WorktreeError('manifest-corrupt', defaultMessage('manifest-corrupt'), { file, cause: String(error) })
1430
+ }
1431
+ }
1432
+
1433
+ private async writeManifest(repoPath: string, manifest: ParsedWorktreeManifest): Promise<void> {
1434
+ const file = manifestPath(repoPath)
1435
+ await mkdir(dirname(file), { recursive: true })
1436
+ await writeFileAtomic(file, JSON.stringify(manifest, null, 2) + '\n', { mode: 0o644 })
1437
+ // Every plugin mutation lands here, so the status cache can never serve a
1438
+ // pre-mutation answer for a lane the user just created, bound or removed.
1439
+ this.invalidateStatus(repoPath)
1440
+ }
1441
+
1442
+ private async appendManifestEntry(repoPath: string, entry: WorktreeManifestEntry): Promise<void> {
1443
+ const manifest = await this.readManifest(repoPath)
1444
+ if (manifest.worktrees.some((e) => e.name === entry.name)) {
1445
+ throw new WorktreeError('worktree-exists', defaultMessage('worktree-exists'), { name: entry.name })
1446
+ }
1447
+ manifest.worktrees.push(entry)
1448
+ await this.writeManifest(repoPath, manifest)
1449
+ }
1450
+
1451
+ private async removeManifestEntry(repoPath: string, name: string): Promise<void> {
1452
+ const manifest = await this.readManifest(repoPath)
1453
+ manifest.worktrees = manifest.worktrees.filter((e) => e.name !== name)
1454
+ await this.writeManifest(repoPath, manifest)
1455
+ }
1456
+
1457
+ private async ensureWorkspaceDir(repoPath: string): Promise<void> {
1458
+ await mkdir(worktreeRoot(repoPath), { recursive: true })
1459
+ }
1460
+
1461
+ private async headBranch(cwd: string): Promise<string | null> {
1462
+ const r = await this.runner.run(['symbolic-ref', '-q', '--short', 'HEAD'], cwd)
1463
+ if (r.exitCode !== 0) return null
1464
+ const trimmed = r.stdout.trim()
1465
+ return trimmed === '' ? null : trimmed
1466
+ }
1467
+
1468
+ private async headShort(cwd: string): Promise<string | null> {
1469
+ const r = await this.runner.run(['rev-parse', '--short', 'HEAD'], cwd)
1470
+ if (r.exitCode !== 0) return null
1471
+ const trimmed = r.stdout.trim()
1472
+ return trimmed === '' ? null : trimmed
1473
+ }
1474
+
1475
+ private async dirtyCount(cwd: string): Promise<number | null> {
1476
+ if (!existsSync(cwd)) return null
1477
+ const r = await this.runner.run(['status', '--porcelain'], cwd)
1478
+ if (r.exitCode !== 0) return null
1479
+ return r.stdout.split('\n').filter((line) => line.trim() !== '').length
1480
+ }
1481
+ }
1482
+
1483
+ // ─── module-level helpers ────────────────────────────────────────────────────
1484
+
1485
+ interface ParsedWorktree {
1486
+ path: string
1487
+ branch: string | null
1488
+ head: string | null
1489
+ }
1490
+
1491
+ /** Parse the `git worktree list --porcelain` output. */
1492
+ function parsePorcelainWorktrees(stdout: string): ParsedWorktree[] {
1493
+ const out: ParsedWorktree[] = []
1494
+ let current: ParsedWorktree | null = null
1495
+ for (const raw of stdout.split('\n')) {
1496
+ const line = raw.replace(/\r$/, '')
1497
+ if (line.startsWith('worktree ')) {
1498
+ if (current !== null) out.push(current)
1499
+ current = { path: line.slice('worktree '.length).trim(), branch: null, head: null }
1500
+ } else if (current !== null && line.startsWith('HEAD ')) {
1501
+ current.head = line.slice('HEAD '.length).trim() || null
1502
+ } else if (current !== null && line.startsWith('branch ')) {
1503
+ const ref = line.slice('branch '.length).trim()
1504
+ current.branch = ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref
1505
+ } else if (current !== null && line === 'detached') {
1506
+ current.branch = null
1507
+ }
1508
+ }
1509
+ if (current !== null) out.push(current)
1510
+ return out
1511
+ }
1512
+
1513
+ function basename(path: string): string {
1514
+ const segments = path.replace(/[\\/]+$/, '').split(/[\\/]/)
1515
+ return segments[segments.length - 1] ?? path
1516
+ }
1517
+
1518
+ function deriveNameFromPath(path: string): string {
1519
+ return basename(path)
1520
+ }
1521
+
1522
+ /**
1523
+ * Look a service up the way cordis exposes it (`ctx.get(name, false)`) and fall
1524
+ * back to the property alias. Both shapes exist across DSH versions, and a
1525
+ * silent miss here means the header chip and the lane tool report "no lane"
1526
+ * while the panel shows the lane from the manifest.
1527
+ */
1528
+ function optionalService<T>(ctx: Context, name: string): T | undefined {
1529
+ const get = (ctx as { get?: (id: string, strict?: boolean) => unknown }).get
1530
+ if (typeof get === 'function') {
1531
+ try {
1532
+ const found = get.call(ctx, name, false) as T | undefined
1533
+ if (found !== undefined && found !== null) return found
1534
+ } catch {
1535
+ // fall through to the property alias
1536
+ }
1537
+ }
1538
+ return (ctx as unknown as Record<string, T | undefined>)[name]
1539
+ }
1540
+
1541
+ /** The real path when it exists; the trimmed path otherwise (never ''). */
1542
+ function canonicalPath(path: string): string | null {
1543
+ if (path === '') return null
1544
+ try {
1545
+ return realpathSync.native(path)
1546
+ } catch {
1547
+ const trimmed = path.replace(/[\\/]+$/, '')
1548
+ return trimmed === '' ? null : trimmed
1549
+ }
1550
+ }
1551
+
1552
+ // Re-export for callers/tests.
1553
+ export { WORKSPACE_DIR, worktreeDir, worktreeRoot }