mixdog 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/README.md +1 -0
  2. package/package.json +2 -1
  3. package/scripts/lib/run-node-tests.mjs +9 -0
  4. package/scripts/prune-desktop-runtime.mjs +3 -0
  5. package/scripts/test-direct.mjs +4 -2
  6. package/src/defaults/shipped-history.json +6 -0
  7. package/src/defaults/skills/setup/references/actions.md +5 -2
  8. package/src/defaults/skills/visualize/SKILL.md +157 -0
  9. package/src/defaults/skills/visualize/references/craft.md +88 -0
  10. package/src/headless-command.mjs +56 -3
  11. package/src/headless-exec.mjs +17 -1
  12. package/src/help.mjs +2 -0
  13. package/src/rules/shared/70-delivery.md +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  16. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  17. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  18. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  19. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/config.mjs +9 -1
  21. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
  22. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
  23. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
  24. package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
  25. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  27. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  28. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
  29. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  30. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  32. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
  33. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  34. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  37. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  38. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  39. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  40. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
  41. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
  42. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  43. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  44. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  45. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  50. package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  53. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
  54. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
  57. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  58. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  59. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  60. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  61. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
  62. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  63. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  65. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  66. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  69. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
  70. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
  71. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
  72. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
  76. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  77. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  78. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  79. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  80. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  81. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  82. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  83. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  84. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  85. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  86. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  87. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  88. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
  89. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  90. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  91. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  92. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
  93. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
  94. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
  95. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  96. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
  97. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  99. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  100. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  101. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
  103. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  113. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  114. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  115. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  116. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  117. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  118. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  119. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  120. package/src/runtime/attachments/store.mjs +38 -0
  121. package/src/runtime/shared/atomic-file.mjs +8 -2
  122. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  123. package/src/runtime/shared/code-graph-state.mjs +12 -0
  124. package/src/runtime/shared/defaults-separation.mjs +2 -0
  125. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  126. package/src/runtime/shared/git-runner.mjs +4 -3
  127. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  128. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  129. package/src/runtime/shared/llm/model-catalog.mjs +240 -118
  130. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  131. package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
  132. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  133. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  134. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  135. package/src/runtime/shared/provider-accounts.mjs +2 -2
  136. package/src/runtime/shared/provider-api-key.mjs +21 -1
  137. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  138. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  139. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  140. package/src/runtime/shared/singleton-owner.mjs +36 -6
  141. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  142. package/src/runtime/shared/tool-surface.mjs +7 -0
  143. package/src/runtime/shared/tool-work-units.mjs +14 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  145. package/src/runtime/shared/turn-changes.mjs +791 -0
  146. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  147. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  148. package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
  149. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  150. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  151. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  152. package/src/runtime/shared/worktrees/git.mjs +247 -0
  153. package/src/runtime/shared/worktrees/include.mjs +480 -0
  154. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  155. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  156. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  157. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  158. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  159. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  160. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  161. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  162. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  163. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  164. package/src/runtime/visualize-bridge/client.mjs +181 -0
  165. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  166. package/src/session-runtime/boot/apis.mjs +35 -0
  167. package/src/session-runtime/boot/config.mjs +2 -0
  168. package/src/session-runtime/boot/facade.mjs +20 -0
  169. package/src/session-runtime/boot/infrastructure.mjs +1 -0
  170. package/src/session-runtime/boot/session.mjs +1 -0
  171. package/src/session-runtime/boot/tools.mjs +4 -0
  172. package/src/session-runtime/config-lifecycle.mjs +10 -1
  173. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
  174. package/src/session-runtime/global-extensions.mjs +2 -2
  175. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  176. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  177. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  178. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  179. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  180. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  181. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  182. package/src/session-runtime/mcp-glue/connect.mjs +11 -6
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
  184. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  185. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  186. package/src/session-runtime/model-route/set-route.mjs +15 -2
  187. package/src/session-runtime/notification-bus.mjs +9 -1
  188. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  189. package/src/session-runtime/resource-extension-sync.mjs +30 -7
  190. package/src/session-runtime/resource-mcp-api.mjs +20 -7
  191. package/src/session-runtime/route-resolution.mjs +10 -1
  192. package/src/session-runtime/route-state.mjs +16 -2
  193. package/src/session-runtime/runtime-core.mjs +5 -0
  194. package/src/session-runtime/runtime-facade.mjs +8 -0
  195. package/src/session-runtime/runtime-review-api.mjs +9 -0
  196. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  197. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  198. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  199. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  200. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  201. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  202. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  203. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  204. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  205. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  206. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  207. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  208. package/src/session-runtime/services/agent-tool.mjs +1 -0
  209. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  210. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  211. package/src/session-runtime/session-create/session-options.mjs +10 -1
  212. package/src/session-runtime/session-turn-api.mjs +1 -0
  213. package/src/session-runtime/settings-api.mjs +4 -2
  214. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  215. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  216. package/src/session-runtime/setup-tool/executor.mjs +11 -4
  217. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
  218. package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
  219. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  220. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
  221. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  222. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  223. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  224. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  225. package/src/session-runtime/turn/turn-run.mjs +6 -7
  226. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  227. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  228. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  229. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  230. package/src/session-runtime/workspace/transitions.mjs +651 -0
  231. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  232. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  233. package/src/standalone/channel-transport.mjs +13 -1
  234. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  235. package/src/standalone/daemon-owner.mjs +372 -0
  236. package/src/standalone/daemon-stored-session-views.mjs +4 -1
  237. package/src/standalone/daemon.mjs +139 -5
  238. package/src/standalone/desktop-service-registry.mjs +20 -0
  239. package/src/standalone/local-session-runtime.mjs +11 -0
  240. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  241. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  242. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  243. package/src/standalone/session-client.mjs +104 -13
  244. package/src/standalone/session-protocol.mjs +16 -0
  245. package/src/standalone/session-proxy.mjs +3 -0
  246. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  247. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  248. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  249. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  250. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  251. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  252. package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
  253. package/src/standalone/session-transport/call-route.mjs +13 -0
  254. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  255. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  256. package/src/standalone/session-transport/client-registry.mjs +20 -3
  257. package/src/standalone/session-transport/routes.mjs +79 -2
  258. package/src/standalone/session-transport/stack.mjs +2 -0
  259. package/src/standalone/session-transport.mjs +24 -1
  260. package/src/tui/app/message-selector.mjs +2 -1
  261. package/src/tui/app/model-options.mjs +35 -5
  262. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  263. package/src/tui/app/model-picker.mjs +4 -2
  264. package/src/tui/dist/index.mjs +332 -36
  265. package/src/tui/index.jsx +10 -2
  266. package/src/tui/session/compaction-view.mjs +147 -0
  267. package/src/tui/session/context-state.mjs +6 -0
  268. package/src/tui/session/initial-state.mjs +3 -0
  269. package/src/tui/session/quota-resume.mjs +2 -1
  270. package/src/tui/session/session-api/commands.mjs +10 -2
  271. package/src/tui/session/session-api/controls.mjs +4 -0
  272. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  273. package/src/tui/session/session-api/integrations.mjs +17 -0
  274. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  275. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  276. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  277. package/src/tui/session/session-api/routes.mjs +10 -1
  278. package/src/tui/session/session-api/settings.mjs +2 -0
  279. package/src/tui/session/session-api/worktree.mjs +60 -0
  280. package/src/tui/session/session-api.mjs +2 -0
  281. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  282. package/src/tui/session/session-flow/drain.mjs +15 -1
  283. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  284. package/src/tui/session/session-flow.mjs +2 -0
  285. package/src/tui/session/transcript-intake.mjs +14 -5
  286. package/src/tui/session/turn-stream.mjs +32 -4
  287. package/src/tui/session/turn-tool-cards.mjs +10 -2
  288. package/src/tui/session/turn.mjs +39 -10
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Shared contracts for Lead-session git worktrees.
3
+ *
4
+ * Pure shapes, validators and constants every worktree scope builds on: the
5
+ * session workspace record (kept separate from project identity), the managed
6
+ * worktree registry entry, the owner record, the branch occupancy DTO, stable
7
+ * error codes, the managed path layout and the per-root cleanup registry.
8
+ * Nothing here touches git or the filesystem.
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ import { join, resolve } from 'node:path';
12
+ import { isPlainObject } from '../object.mjs';
13
+ import { resolvePluginData } from '../plugin-paths.mjs';
14
+
15
+ /** Versioned directory under the data dir that holds every managed worktree. */
16
+ export const WORKTREE_STORE_DIR = 'worktrees-v1';
17
+ /** Default ref prefix for branches created for managed worktrees. */
18
+ export const DEFAULT_WORKTREE_BRANCH_PREFIX = 'mixdog/';
19
+
20
+ export const WORKSPACE_TRANSITION_STATES = Object.freeze(['idle', 'provisioning', 'switching', 'removing', 'recovery']);
21
+ const WORKTREE_ENTRY_STATES = Object.freeze(['creating', 'ready', 'deleting', 'recovery']);
22
+ const BRANCH_OCCUPANCY_KINDS = Object.freeze(['current', 'session', 'external', 'free']);
23
+
24
+ /**
25
+ * Stable error codes. Clients branch on `error.code`, never on message text.
26
+ * @readonly
27
+ */
28
+ export const WORKTREE_ERROR_CODES = Object.freeze({
29
+ WORKTREE_BUSY_JOBS: 'WORKTREE_BUSY_JOBS',
30
+ WORKTREE_BRANCH_OCCUPIED: 'WORKTREE_BRANCH_OCCUPIED',
31
+ WORKTREE_DISK_INSUFFICIENT: 'WORKTREE_DISK_INSUFFICIENT',
32
+ WORKTREE_LARGE_REPO_CONFIRM: 'WORKTREE_LARGE_REPO_CONFIRM',
33
+ WORKTREE_DIRTY: 'WORKTREE_DIRTY',
34
+ WORKTREE_UNMERGED: 'WORKTREE_UNMERGED',
35
+ WORKTREE_MISSING: 'WORKTREE_MISSING',
36
+ WORKTREE_UNSUPPORTED_REPO: 'WORKTREE_UNSUPPORTED_REPO',
37
+ WORKTREE_REVISION_CONFLICT: 'WORKTREE_REVISION_CONFLICT',
38
+ WORKTREE_NOT_IMPLEMENTED: 'WORKTREE_NOT_IMPLEMENTED',
39
+ WORKTREE_CONFIRM_REQUIRED: 'WORKTREE_CONFIRM_REQUIRED',
40
+ WORKTREE_TARGET_MISMATCH: 'WORKTREE_TARGET_MISMATCH',
41
+ WORKTREE_PROJECT_MISMATCH: 'WORKTREE_PROJECT_MISMATCH',
42
+ WORKTREE_PARTIAL_TRANSITION: 'WORKTREE_PARTIAL_TRANSITION',
43
+ WORKTREE_PROJECT_UNKNOWN: 'WORKTREE_PROJECT_UNKNOWN',
44
+ WORKTREE_REMOVAL_INCOMPLETE: 'WORKTREE_REMOVAL_INCOMPLETE',
45
+ WORKTREE_PATH_UNSAFE: 'WORKTREE_PATH_UNSAFE',
46
+ MERGE_CONFLICT: 'MERGE_CONFLICT',
47
+ MERGE_TARGET_DIRTY: 'MERGE_TARGET_DIRTY',
48
+ MERGE_FAILED: 'MERGE_FAILED',
49
+ });
50
+
51
+ /** @typedef {keyof typeof WORKTREE_ERROR_CODES} WorktreeErrorCode */
52
+
53
+ const DEFAULT_ERROR_MESSAGES = Object.freeze({
54
+ WORKTREE_BUSY_JOBS: 'The session has running jobs; wait for them or stop them before changing its workspace.',
55
+ WORKTREE_BRANCH_OCCUPIED: 'The branch is checked out in another worktree.',
56
+ WORKTREE_DISK_INSUFFICIENT: 'Not enough free disk space to create the worktree.',
57
+ WORKTREE_LARGE_REPO_CONFIRM: 'The repository is large; confirm before creating a worktree.',
58
+ WORKTREE_DIRTY: 'The worktree has uncommitted changes.',
59
+ WORKTREE_UNMERGED: 'The worktree branch has commits that are not merged.',
60
+ WORKTREE_MISSING: 'The managed worktree no longer exists.',
61
+ WORKTREE_UNSUPPORTED_REPO: 'This repository does not support managed worktrees.',
62
+ WORKTREE_REVISION_CONFLICT: 'The session workspace changed; reload and try again.',
63
+ WORKTREE_NOT_IMPLEMENTED: 'Worktree support is not implemented yet.',
64
+ WORKTREE_CONFIRM_REQUIRED: 'A merge requires explicit confirmation.',
65
+ WORKTREE_TARGET_MISMATCH: 'The project folder is not on the branch the merge was confirmed for.',
66
+ WORKTREE_PROJECT_MISMATCH: 'The retained worktree belongs to another project or repository.',
67
+ WORKTREE_PARTIAL_TRANSITION: 'The branch change failed part-way; the project folder stayed on the new branch.',
68
+ WORKTREE_PROJECT_UNKNOWN: 'Project is not available.',
69
+ WORKTREE_REMOVAL_INCOMPLETE:
70
+ 'The worktree was only partly removed (a file or folder is still in use); remove it again to finish.',
71
+ WORKTREE_PATH_UNSAFE: 'The worktree folder is not a managed checkout of this repository; nothing was deleted.',
72
+ MERGE_CONFLICT: 'The merge would conflict.',
73
+ MERGE_TARGET_DIRTY: 'The merge target has uncommitted changes.',
74
+ MERGE_FAILED: 'The merge failed; the project folder is unchanged.',
75
+ });
76
+
77
+ /** Typed worktree failure; `code` is one of WORKTREE_ERROR_CODES. */
78
+ export class WorktreeError extends Error {
79
+ /**
80
+ * @param {WorktreeErrorCode} code
81
+ * @param {string} [message]
82
+ * @param {Record<string, unknown>} [details] JSON-safe context (paths, branch, counts).
83
+ */
84
+ constructor(code, message, details) {
85
+ super(message || DEFAULT_ERROR_MESSAGES[code] || code);
86
+ this.name = 'WorktreeError';
87
+ this.code = code;
88
+ if (details !== undefined) this.details = details;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Build a typed worktree error. Unknown codes are a programming error.
94
+ * @param {WorktreeErrorCode} code
95
+ * @param {string} [message]
96
+ * @param {Record<string, unknown>} [details]
97
+ * @returns {WorktreeError}
98
+ */
99
+ export function worktreeError(code, message, details) {
100
+ if (!Object.hasOwn(WORKTREE_ERROR_CODES, code)) throw new TypeError(`Unknown worktree error code "${code}".`);
101
+ return new WorktreeError(code, message, details);
102
+ }
103
+
104
+ /**
105
+ * @param {unknown} error
106
+ * @param {WorktreeErrorCode} [code] When given, also require this code.
107
+ * @returns {boolean}
108
+ */
109
+ export function isWorktreeError(error, code) {
110
+ const actual = /** @type {{code?: unknown}} */ (error)?.code;
111
+ if (typeof actual !== 'string' || !Object.hasOwn(WORKTREE_ERROR_CODES, actual)) return false;
112
+ return code === undefined || actual === code;
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Field validators
117
+
118
+ function text(value, name) {
119
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${name} must be a non-empty string.`);
120
+ return value;
121
+ }
122
+
123
+ function nullableText(value, name) {
124
+ return value === undefined || value === null ? null : text(value, name);
125
+ }
126
+
127
+ function oneOf(value, allowed, name) {
128
+ if (!allowed.includes(value)) throw new TypeError(`${name} must be one of ${allowed.join(', ')}.`);
129
+ return value;
130
+ }
131
+
132
+ const COMMIT_PATTERN = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
133
+
134
+ function nullableCommit(value, name) {
135
+ if (value === undefined || value === null) return null;
136
+ if (typeof value !== 'string' || !COMMIT_PATTERN.test(value)) {
137
+ throw new TypeError(`${name} must be a full lowercase commit id.`);
138
+ }
139
+ return value;
140
+ }
141
+
142
+ function record(value, name) {
143
+ if (!isPlainObject(value)) throw new TypeError(`${name} must be an object.`);
144
+ return value;
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Session workspace
149
+
150
+ /**
151
+ * @typedef {'idle'|'provisioning'|'switching'|'removing'|'recovery'} WorkspaceTransitionState
152
+ * @typedef {{ state: WorkspaceTransitionState, txId: string|null }} WorkspaceTransition
153
+ * `txId` is null exactly when `state` is idle.
154
+ */
155
+
156
+ /**
157
+ * Where a Lead session executes. Project identity (`projectPath`) stays the
158
+ * registered Project even while `cwd` points into a managed worktree.
159
+ * @typedef {object} SessionWorkspace
160
+ * @property {string} projectPath Registered Project root.
161
+ * @property {string|null} repoCommonDir Canonical git common dir; null outside a git repository.
162
+ * @property {string} cwd Execution root for the Lead and its agents.
163
+ * @property {string|null} worktreeId Managed worktree id; null when running in the project folder.
164
+ * @property {string|null} branch Checked-out branch at `cwd`; null when detached or not a repository.
165
+ * @property {string|null} baseCommit Commit the worktree branch was created from.
166
+ * @property {string|null} mergeTarget Branch the worktree merges into (the project folder's branch).
167
+ * @property {number} revision Monotonic; advances on every committed workspace change.
168
+ * @property {WorkspaceTransition} transition In-flight transition marker.
169
+ */
170
+
171
+ /**
172
+ * Validate and normalize a session workspace record. Throws TypeError on any
173
+ * invalid field or broken invariant; returns a fresh plain object.
174
+ * @param {unknown} input
175
+ * @returns {SessionWorkspace}
176
+ */
177
+ export function normalizeSessionWorkspace(input) {
178
+ const value = record(input, 'workspace');
179
+ const transitionInput = value.transition === undefined ? { state: 'idle', txId: null } : value.transition;
180
+ const transitionRecord = record(transitionInput, 'workspace.transition');
181
+ const state = oneOf(transitionRecord.state, WORKSPACE_TRANSITION_STATES, 'workspace.transition.state');
182
+ const txId = nullableText(transitionRecord.txId, 'workspace.transition.txId');
183
+ if ((state === 'idle') !== (txId === null)) {
184
+ throw new TypeError('workspace.transition.txId must be null exactly when the state is idle.');
185
+ }
186
+ const revision = value.revision === undefined ? 0 : value.revision;
187
+ if (!Number.isSafeInteger(revision) || revision < 0) {
188
+ throw new TypeError('workspace.revision must be a non-negative integer.');
189
+ }
190
+ const workspace = {
191
+ projectPath: text(value.projectPath, 'workspace.projectPath'),
192
+ repoCommonDir: nullableText(value.repoCommonDir, 'workspace.repoCommonDir'),
193
+ cwd: text(value.cwd, 'workspace.cwd'),
194
+ worktreeId: nullableText(value.worktreeId, 'workspace.worktreeId'),
195
+ branch: nullableText(value.branch, 'workspace.branch'),
196
+ baseCommit: nullableCommit(value.baseCommit, 'workspace.baseCommit'),
197
+ mergeTarget: nullableText(value.mergeTarget, 'workspace.mergeTarget'),
198
+ revision,
199
+ transition: { state, txId },
200
+ };
201
+ if (workspace.worktreeId !== null && (workspace.repoCommonDir === null || workspace.branch === null)) {
202
+ throw new TypeError('A worktree-bound workspace requires repoCommonDir and branch.');
203
+ }
204
+ return workspace;
205
+ }
206
+
207
+ /**
208
+ * The project-folder workspace a new Lead session starts in.
209
+ * @param {{ projectPath: string, cwd?: string, repoCommonDir?: string|null, branch?: string|null }} input
210
+ * @returns {SessionWorkspace}
211
+ */
212
+ export function projectWorkspace({ projectPath, cwd = projectPath, repoCommonDir = null, branch = null }) {
213
+ return normalizeSessionWorkspace({ projectPath, cwd, repoCommonDir, branch, revision: 0 });
214
+ }
215
+
216
+ /** @param {SessionWorkspace|null|undefined} workspace */
217
+ export function isWorktreeBound(workspace) {
218
+ return typeof workspace?.worktreeId === 'string' && workspace.worktreeId.length > 0;
219
+ }
220
+
221
+ /** @param {SessionWorkspace} workspace @returns {number} The revision a committed change must carry. */
222
+ export function nextRevision(workspace) {
223
+ const revision = workspace?.revision;
224
+ if (!Number.isSafeInteger(revision) || revision < 0) throw new TypeError('workspace.revision is invalid.');
225
+ return revision + 1;
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Managed worktree registry entry and owner
230
+
231
+ /**
232
+ * @typedef {'creating'|'ready'|'deleting'|'recovery'} WorktreeEntryState
233
+ * @typedef {object} WorktreeEntry
234
+ * @property {string} id Slug, unique per repository.
235
+ * @property {string} repoCommonDir
236
+ * @property {string} projectPath
237
+ * @property {string} path Absolute worktree folder (see managedWorktreePath).
238
+ * @property {string} branch Full branch name, e.g. "mixdog/fix-login".
239
+ * @property {string|null} baseCommit
240
+ * @property {number} createdAt Epoch milliseconds.
241
+ * @property {string|null} ownerSessionId Lead session that retains it; null once released.
242
+ * @property {WorktreeEntryState} state
243
+ */
244
+
245
+ /**
246
+ * @param {unknown} input
247
+ * @returns {WorktreeEntry}
248
+ */
249
+ export function normalizeWorktreeEntry(input) {
250
+ const value = record(input, 'worktree');
251
+ if (!Number.isSafeInteger(value.createdAt) || value.createdAt < 0) {
252
+ throw new TypeError('worktree.createdAt must be epoch milliseconds.');
253
+ }
254
+ const id = text(value.id, 'worktree.id');
255
+ const idError = worktreeNameError(id);
256
+ if (idError) throw new TypeError(`worktree.id is invalid: ${idError}`);
257
+ return {
258
+ id,
259
+ repoCommonDir: text(value.repoCommonDir, 'worktree.repoCommonDir'),
260
+ projectPath: text(value.projectPath, 'worktree.projectPath'),
261
+ path: text(value.path, 'worktree.path'),
262
+ branch: text(value.branch, 'worktree.branch'),
263
+ baseCommit: nullableCommit(value.baseCommit, 'worktree.baseCommit'),
264
+ createdAt: value.createdAt,
265
+ ownerSessionId: nullableText(value.ownerSessionId, 'worktree.ownerSessionId'),
266
+ state: oneOf(value.state, WORKTREE_ENTRY_STATES, 'worktree.state'),
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Session-scoped ownership: a restarted runtime of the same session keeps the
272
+ * worktree; `runtimeToken` tells a live owner from a stale one and `pid` is a
273
+ * liveness hint only.
274
+ * @typedef {{ sessionId: string, runtimeToken: string, pid: number|null }} WorktreeOwner
275
+ */
276
+
277
+ /**
278
+ * @param {unknown} input
279
+ * @returns {WorktreeOwner}
280
+ */
281
+ export function normalizeWorktreeOwner(input) {
282
+ const value = record(input, 'owner');
283
+ const pid = value.pid === undefined || value.pid === null ? null : value.pid;
284
+ if (pid !== null && (!Number.isSafeInteger(pid) || pid <= 0)) {
285
+ throw new TypeError('owner.pid must be a positive integer or null.');
286
+ }
287
+ return {
288
+ sessionId: text(value.sessionId, 'owner.sessionId'),
289
+ runtimeToken: text(value.runtimeToken, 'owner.runtimeToken'),
290
+ pid,
291
+ };
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Branch occupancy
296
+
297
+ /**
298
+ * Who holds a branch. `current`: checked out in the project folder; `session`:
299
+ * a managed worktree retained by `sessionId`; `external`: another worktree not
300
+ * managed by Mixdog; `free`: not checked out anywhere.
301
+ * @typedef {'current'|'session'|'external'|'free'} BranchOccupancyKind
302
+ * @typedef {{ branch: string, kind: BranchOccupancyKind, sessionId?: string, path?: string }} BranchOccupancy
303
+ */
304
+
305
+ /**
306
+ * @param {unknown} input
307
+ * @returns {BranchOccupancy}
308
+ */
309
+ export function normalizeBranchOccupancy(input) {
310
+ const value = record(input, 'occupancy');
311
+ const branch = text(value.branch, 'occupancy.branch');
312
+ const kind = oneOf(value.kind, BRANCH_OCCUPANCY_KINDS, 'occupancy.kind');
313
+ if (kind === 'free') {
314
+ if (value.sessionId != null || value.path != null) {
315
+ throw new TypeError('A free branch carries no sessionId or path.');
316
+ }
317
+ return { branch, kind };
318
+ }
319
+ if (kind !== 'session' && value.sessionId != null) {
320
+ throw new TypeError('Only a session occupancy carries a sessionId.');
321
+ }
322
+ const occupancy = { branch, kind };
323
+ if (kind === 'session') occupancy.sessionId = text(value.sessionId, 'occupancy.sessionId');
324
+ if (value.path != null) occupancy.path = text(value.path, 'occupancy.path');
325
+ return occupancy;
326
+ }
327
+
328
+ /**
329
+ * The branch menu's rows (the desktop DesktopGitBranch shape) for the local
330
+ * branch names the manager lists: `current` marks the branch checked out in
331
+ * the project folder; managed lists hold local branches only.
332
+ * @param {string[]} names
333
+ * @param {BranchOccupancy[]} occupancy
334
+ * @returns {Array<{ name: string, current: boolean, remote: false, upstream: string }>}
335
+ */
336
+ export function branchMenuEntries(names, occupancy) {
337
+ const current = new Set(occupancy.filter((row) => row.kind === 'current').map((row) => row.branch));
338
+ return names.map((name) => ({ name, current: current.has(name), remote: false, upstream: '' }));
339
+ }
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Names and managed paths
343
+
344
+ const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
345
+ const WORKTREE_NAME_MAX_LENGTH = 64;
346
+
347
+ /**
348
+ * Why `name` cannot be a worktree slug (folder name and branch suffix), or
349
+ * null when it can.
350
+ * @param {unknown} name
351
+ * @returns {string|null}
352
+ */
353
+ export function worktreeNameError(name) {
354
+ if (typeof name !== 'string' || !name) return 'a name is required';
355
+ if (name.length > WORKTREE_NAME_MAX_LENGTH) return `at most ${WORKTREE_NAME_MAX_LENGTH} characters`;
356
+ if (!NAME_PATTERN.test(name)) return 'use letters, digits, ".", "_" or "-", starting with a letter or digit';
357
+ if (name.includes('..') || name.endsWith('.') || name.toLowerCase().endsWith('.lock')) {
358
+ return 'not a valid git branch component';
359
+ }
360
+ return null;
361
+ }
362
+
363
+ /**
364
+ * Stable short hash of a canonical git common dir. Case-insensitive on
365
+ * Windows, where the same repository may be spelled with different casing.
366
+ * @param {string} repoCommonDir
367
+ * @param {NodeJS.Platform} [platform]
368
+ * @returns {string} 16 lowercase hex characters.
369
+ */
370
+ export function repoHash(repoCommonDir, platform = process.platform) {
371
+ let key = resolve(text(repoCommonDir, 'repoCommonDir')).replace(/\\/g, '/').replace(/\/+$/, '');
372
+ if (platform === 'win32') key = key.toLowerCase();
373
+ return createHash('sha256').update(key).digest('hex').slice(0, 16);
374
+ }
375
+
376
+ /**
377
+ * <dataDir>/worktrees-v1/<repoHash> — the root that holds one repository's
378
+ * managed worktrees and the unit runRootCleanups operates on.
379
+ * @param {string} repoCommonDir
380
+ * @param {{ dataDir?: string, platform?: NodeJS.Platform }} [options]
381
+ */
382
+ export function managedWorktreeRoot(repoCommonDir, { dataDir = resolvePluginData(), platform } = {}) {
383
+ return join(dataDir, WORKTREE_STORE_DIR, repoHash(repoCommonDir, platform));
384
+ }
385
+
386
+ /**
387
+ * <dataDir>/worktrees-v1/<repoHash>/<slug>
388
+ * @param {string} repoCommonDir
389
+ * @param {string} slug
390
+ * @param {{ dataDir?: string, platform?: NodeJS.Platform }} [options]
391
+ */
392
+ export function managedWorktreePath(repoCommonDir, slug, options) {
393
+ const error = worktreeNameError(slug);
394
+ if (error) throw new TypeError(`worktree name is invalid: ${error}`);
395
+ return join(managedWorktreeRoot(repoCommonDir, options), slug);
396
+ }
397
+
398
+ /**
399
+ * @param {string} slug
400
+ * @param {string} [prefix]
401
+ * @returns {string} e.g. "mixdog/fix-login"
402
+ */
403
+ export function worktreeBranchName(slug, prefix = DEFAULT_WORKTREE_BRANCH_PREFIX) {
404
+ const error = worktreeNameError(slug);
405
+ if (error) throw new TypeError(`worktree name is invalid: ${error}`);
406
+ return `${prefix}${slug}`;
407
+ }
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // Root cleanup registry
411
+
412
+ /**
413
+ * Cleanup a module performs for one managed root (or a worktree path inside
414
+ * it) when that root is removed: caches, indexes, snapshots keyed by path.
415
+ * @callback RootCleanup
416
+ * @param {string} root Absolute path being removed.
417
+ * @param {{ signal?: AbortSignal, reason?: string } & Record<string, unknown>} ctx
418
+ * @returns {Promise<unknown>|unknown} JSON-safe result for diagnostics.
419
+ */
420
+
421
+ /**
422
+ * @typedef {{ name: string, ok: true, result: unknown } | { name: string, ok: false, error: { code: string|null, message: string } }} RootCleanupResult
423
+ */
424
+
425
+ /** A fresh, independent registry (tests and isolated hosts). */
426
+ export function createRootCleanupRegistry() {
427
+ /** @type {Map<string, RootCleanup>} */
428
+ const cleanups = new Map();
429
+ return {
430
+ /**
431
+ * @param {string} name Unique module name.
432
+ * @param {RootCleanup} cleanup
433
+ * @returns {() => void} Unregister.
434
+ */
435
+ register(name, cleanup) {
436
+ text(name, 'cleanup name');
437
+ if (typeof cleanup !== 'function') throw new TypeError('cleanup must be a function.');
438
+ if (cleanups.has(name)) throw new TypeError(`Root cleanup "${name}" is already registered.`);
439
+ cleanups.set(name, cleanup);
440
+ return () => {
441
+ if (cleanups.get(name) === cleanup) cleanups.delete(name);
442
+ };
443
+ },
444
+ /**
445
+ * Run every registered cleanup in registration order. One failure never
446
+ * stops the rest; each module reports its own result.
447
+ * @param {string} root
448
+ * @param {Record<string, unknown>} [ctx]
449
+ * @returns {Promise<RootCleanupResult[]>}
450
+ */
451
+ async run(root, ctx = {}) {
452
+ text(root, 'root');
453
+ const results = [];
454
+ for (const [name, cleanup] of [...cleanups]) {
455
+ try {
456
+ results.push({ name, ok: true, result: (await cleanup(root, ctx)) ?? null });
457
+ } catch (error) {
458
+ results.push({
459
+ name,
460
+ ok: false,
461
+ error: {
462
+ code: typeof error?.code === 'string' ? error.code : null,
463
+ message: String(error?.message || error),
464
+ },
465
+ });
466
+ }
467
+ }
468
+ return results;
469
+ },
470
+ /** @returns {string[]} */
471
+ names() {
472
+ return [...cleanups.keys()];
473
+ },
474
+ };
475
+ }
476
+
477
+ const processRootCleanups = createRootCleanupRegistry();
478
+
479
+ /**
480
+ * Register a process-wide cleanup for removed worktree roots.
481
+ * @param {string} name
482
+ * @param {RootCleanup} cleanup
483
+ * @returns {() => void} Unregister.
484
+ */
485
+ export function registerRootCleanup(name, cleanup) {
486
+ return processRootCleanups.register(name, cleanup);
487
+ }
488
+
489
+ /**
490
+ * @param {string} root
491
+ * @param {Record<string, unknown>} [ctx]
492
+ * @returns {Promise<RootCleanupResult[]>}
493
+ */
494
+ export function runRootCleanups(root, ctx) {
495
+ return processRootCleanups.run(root, ctx);
496
+ }
497
+
498
+ const processRootReleasers = createRootCleanupRegistry();
499
+
500
+ /**
501
+ * Register a process-wide releaser: it drops live handles that keep a root
502
+ * open (desktop terminals started there). Releasers run before the root
503
+ * cleanups and before the checkout is removed.
504
+ * @param {string} name
505
+ * @param {RootCleanup} releaser
506
+ * @returns {() => void} Unregister.
507
+ */
508
+ export function registerRootReleaser(name, releaser) {
509
+ return processRootReleasers.register(name, releaser);
510
+ }
511
+
512
+ /**
513
+ * @param {string} root
514
+ * @param {Record<string, unknown>} [ctx]
515
+ * @returns {Promise<RootCleanupResult[]>}
516
+ */
517
+ export function runRootReleasers(root, ctx) {
518
+ return processRootReleasers.run(root, ctx);
519
+ }
520
+
521
+ // ---------------------------------------------------------------------------
522
+ // Transition API (interface only; implemented by the workspace transition scope)
523
+
524
+ /**
525
+ * Where a transition moves the session.
526
+ * @typedef {{ kind: 'branch', branch: string, create?: boolean }
527
+ * | { kind: 'worktree', name?: string|null }
528
+ * | { kind: 'remove-worktree', force?: boolean }} WorkspaceTransitionTarget
529
+ * `branch`: run in the project folder on `branch` (the worktree stays retained);
530
+ * `create: true` first creates `branch` from the project folder's current branch;
531
+ * `worktree`: run in this Lead's retained worktree, creating it (optionally named) on first use;
532
+ * `remove-worktree`: delete the retained worktree after dirty/unmerged checks.
533
+ */
534
+
535
+ /**
536
+ * @typedef {object} WorkspaceTransitionOptions
537
+ * @property {number} [expectedRevision] Reject with WORKTREE_REVISION_CONFLICT when the session moved on.
538
+ * @property {boolean} [confirmLarge] Acknowledges WORKTREE_LARGE_REPO_CONFIRM.
539
+ * @property {AbortSignal} [signal]
540
+ */
541
+
542
+ /**
543
+ * Serialized, revisioned workspace change for one Lead session. Fails with a
544
+ * WorktreeError (WORKTREE_BUSY_JOBS, WORKTREE_BRANCH_OCCUPIED, ...) and leaves
545
+ * the previous workspace intact on failure.
546
+ * @callback ApplyWorkspaceTransition
547
+ * @param {{ sessionId: string }} sessionRef
548
+ * @param {WorkspaceTransitionTarget} target
549
+ * @param {WorkspaceTransitionOptions} [options]
550
+ * @returns {Promise<{ revision: number, workspace: SessionWorkspace }>}
551
+ */
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Disk estimate for a new worktree: tracked checkout bytes (blob sizes of the
3
+ * base commit's tree) + include copies + a safety margin, compared with free
4
+ * space on the DESTINATION volume.
5
+ */
6
+ import { existsSync } from 'node:fs';
7
+ import { statfs } from 'node:fs/promises';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { runGit } from './git.mjs';
10
+
11
+ const MARGIN_MIN_BYTES = 64 * 1024 * 1024;
12
+ const MARGIN_RATIO = 0.1;
13
+
14
+ /**
15
+ * Sum of blob sizes in `commit`'s tree; gitlinks flag submodules.
16
+ * @returns {Promise<{ bytes: number, files: number, submodules: boolean }>}
17
+ */
18
+ export async function trackedCheckoutBytes(cwd, commit, { signal } = {}) {
19
+ const result = await runGit(cwd, ['ls-tree', '-r', '-l', '-z', '--full-tree', commit], {
20
+ signal,
21
+ timeoutMs: 600_000,
22
+ maxBytes: 1024 * 1024 * 1024,
23
+ });
24
+ let bytes = 0;
25
+ let files = 0;
26
+ let submodules = false;
27
+ for (const record of result.stdout.split('\0')) {
28
+ if (!record) continue;
29
+ const tab = record.indexOf('\t');
30
+ const [, type, , size] = record.slice(0, tab).trim().split(/\s+/);
31
+ if (type === 'commit') {
32
+ submodules = true;
33
+ continue;
34
+ }
35
+ if (type !== 'blob') continue;
36
+ files += 1;
37
+ bytes += Number(size) || 0;
38
+ }
39
+ return { bytes, files, submodules };
40
+ }
41
+
42
+ /** Bytes needed for `contentBytes` of checkout and copies. */
43
+ export function requiredBytes(contentBytes) {
44
+ return contentBytes + Math.max(MARGIN_MIN_BYTES, Math.ceil(contentBytes * MARGIN_RATIO));
45
+ }
46
+
47
+ /** Free bytes available to this user on the volume holding `path`. */
48
+ export async function freeBytesAt(path) {
49
+ let dir = resolve(path);
50
+ while (!existsSync(dir)) {
51
+ const parent = dirname(dir);
52
+ if (parent === dir) break;
53
+ dir = parent;
54
+ }
55
+ const info = await statfs(dir);
56
+ return Number(info.bavail) * Number(info.bsize);
57
+ }