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,480 @@
1
+ /**
2
+ * `.worktreeinclude` (optional, at the repository root): untracked local
3
+ * files a fresh managed worktree should start with. Non-executable format,
4
+ * one entry per line, `#` comments:
5
+ *
6
+ * copy <relpath> copy the file or directory into the worktree
7
+ * link <relpath> link it (directory junction on Windows, symlink elsewhere)
8
+ * <pattern> any other line: a .gitignore-style pattern (the format
9
+ * other tools' `.worktreeinclude` files use); every
10
+ * untracked path it matches that the repository also
11
+ * git-ignores is copied. Explicit copy/link entries win.
12
+ *
13
+ * No entry is ever a creation failure: a line whose path breaks the rules
14
+ * below (or a pattern git cannot use) is skipped with `include-invalid:<line>`,
15
+ * and an entry or match that resolves somewhere it must not (outside the
16
+ * repository, into .git, a Unity Library or project link, through a link in
17
+ * the worktree) is skipped with `include-skipped:<path>`. A skipped entry is
18
+ * never copied or linked.
19
+ *
20
+ * Rules: relative paths only (no absolute, `..`, `.git` or `:`); the source
21
+ * must resolve inside the repository (no escape through links); an existing
22
+ * path in the checkout is never overwritten; a Unity `Library` directory is
23
+ * never copied or linked. A link that cannot be made fails the creation
24
+ * clearly — it never degrades into a (possibly huge) copy.
25
+ */
26
+ import { createHash } from 'node:crypto';
27
+ import { constants as fsConstants } from 'node:fs';
28
+ import {
29
+ cp,
30
+ copyFile,
31
+ link,
32
+ lstat,
33
+ mkdir,
34
+ readdir,
35
+ readFile,
36
+ realpath,
37
+ rmdir,
38
+ stat,
39
+ symlink,
40
+ unlink,
41
+ } from 'node:fs/promises';
42
+ import { dirname, isAbsolute, join, relative, sep } from 'node:path';
43
+ import { isWorktreeError, worktreeError } from './contract.mjs';
44
+ import { runGit } from './git.mjs';
45
+ import { isWithin } from './paths.mjs';
46
+
47
+ const INCLUDE_FILE = '.worktreeinclude';
48
+ const MAX_DIGEST_ENTRIES = 100_000;
49
+ // Paths one set of patterns may select, and per `git check-ignore` call.
50
+ const MAX_PATTERN_MATCHES = 2_000;
51
+ const CHECK_IGNORE_BATCH = 200;
52
+ const UNITY_MARKER = join('ProjectSettings', 'ProjectVersion.txt');
53
+
54
+ function invalid(line, reason) {
55
+ return worktreeError('WORKTREE_UNSUPPORTED_REPO', `${INCLUDE_FILE} line ${line}: ${reason}.`, {
56
+ file: INCLUDE_FILE,
57
+ line,
58
+ });
59
+ }
60
+
61
+ function relKey(rel) {
62
+ return process.platform === 'win32' ? rel.toLowerCase() : rel;
63
+ }
64
+
65
+ /** Normalized forward-slash relative path, or a thrown include error. */
66
+ export function validateIncludePath(value, line = 0) {
67
+ const text = String(value ?? '').trim();
68
+ if (!text) throw invalid(line, 'a path is required');
69
+ if (text.includes('\0')) throw invalid(line, 'the path contains a NUL byte');
70
+ if (isAbsolute(text) || /^[A-Za-z]:/.test(text) || /^[\\/]/.test(text))
71
+ throw invalid(line, 'absolute paths are not allowed');
72
+ const parts = text.split(/[\\/]+/).filter((part) => part && part !== '.');
73
+ if (!parts.length) throw invalid(line, 'the path names the repository root');
74
+ if (parts.includes('..')) throw invalid(line, '".." is not allowed');
75
+ if (parts.some((part) => part.toLowerCase() === '.git')) throw invalid(line, '".git" is not allowed');
76
+ if (parts.some((part) => part.includes(':'))) throw invalid(line, '":" is not allowed');
77
+ return parts.join('/');
78
+ }
79
+
80
+ /**
81
+ * Parse the include file at `repoRoot`. Missing file → []. Bare lines are
82
+ * `{ mode: 'pattern', pattern, line }` (`pattern` null: unusable, warned); a
83
+ * copy/link line whose path breaks the rules is `{ mode: 'invalid', line, reason }`.
84
+ * @returns {Promise<Array<{ mode: 'copy'|'link', rel: string, line: number } | { mode: 'pattern', pattern: string|null, line: number } | { mode: 'invalid', line: number, reason: string }>>}
85
+ */
86
+ export async function readIncludeEntries(repoRoot) {
87
+ let text;
88
+ try {
89
+ text = await readFile(join(repoRoot, INCLUDE_FILE), 'utf8');
90
+ } catch (error) {
91
+ if (error?.code === 'ENOENT') return [];
92
+ throw error;
93
+ }
94
+ const entries = [];
95
+ text.split(/\r?\n/).forEach((raw, index) => {
96
+ const line = raw.replace(/^\uFEFF/, '').trim();
97
+ if (!line || line.startsWith('#')) return;
98
+ const match = /^(copy|link)\s+(.+)$/.exec(line);
99
+ if (!match) {
100
+ // gitignore syntax: `\#` and `\!` escape, a NUL cannot be passed to git.
101
+ entries.push({ mode: 'pattern', pattern: line.includes('\0') ? null : line, line: index + 1 });
102
+ return;
103
+ }
104
+ let rel;
105
+ try {
106
+ rel = validateIncludePath(match[2], index + 1);
107
+ } catch (error) {
108
+ // Absolute, `..`, `.git`, `:`: never followed, never a failure.
109
+ entries.push({ mode: 'invalid', line: index + 1, reason: error.message });
110
+ return;
111
+ }
112
+ entries.push({ mode: /** @type {'copy'|'link'} */ (match[1]), rel, line: index + 1 });
113
+ });
114
+ const explicit = entries.filter((entry) => entry.mode === 'copy' || entry.mode === 'link');
115
+ for (const entry of explicit) {
116
+ for (const other of explicit) {
117
+ if (entry === other) continue;
118
+ const a = relKey(entry.rel);
119
+ const b = relKey(other.rel);
120
+ if (a === b || b.startsWith(`${a}/`)) {
121
+ throw invalid(other.line, `"${other.rel}" overlaps "${entry.rel}" (line ${entry.line})`);
122
+ }
123
+ }
124
+ }
125
+ return entries;
126
+ }
127
+
128
+ async function exists(path) {
129
+ try {
130
+ await lstat(path);
131
+ return true;
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ async function isUnityProject(dir) {
138
+ try {
139
+ return (await stat(join(dir, UNITY_MARKER))).isFile();
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ /** True when `relParts` is, or lies inside, a Unity project's Library. */
146
+ async function insideUnityLibrary(root, relParts) {
147
+ for (let index = 0; index < relParts.length; index++) {
148
+ if (relParts[index].toLowerCase() !== 'library') continue;
149
+ if (await isUnityProject(join(root, ...relParts.slice(0, index)))) return true;
150
+ }
151
+ return false;
152
+ }
153
+
154
+ /** Library dir of a Unity project? (used to filter directory copies) */
155
+ async function isUnityLibraryDir(path) {
156
+ return path.split(sep).at(-1).toLowerCase() === 'library' && isUnityProject(dirname(path));
157
+ }
158
+
159
+ async function treeBytes(path) {
160
+ const info = await lstat(path);
161
+ if (info.isSymbolicLink()) return 0;
162
+ if (!info.isDirectory()) return info.size;
163
+ if (await isUnityLibraryDir(path)) return 0;
164
+ let total = 0;
165
+ for (const name of await readdir(path)) total += await treeBytes(join(path, name));
166
+ return total;
167
+ }
168
+
169
+ /**
170
+ * Resolve and validate every entry against the repository before anything is
171
+ * allocated. Missing sources are skipped with a warning.
172
+ * @param {string} repoRoot
173
+ * @param {Array<{ mode: 'copy'|'link', rel: string, line: number }>} entries
174
+ */
175
+ export async function planIncludes(repoRoot, entries) {
176
+ const rootReal = await realpath(repoRoot);
177
+ const items = [];
178
+ const warnings = [];
179
+ let copyBytes = 0;
180
+ const explicit = entries.filter((entry) => entry.mode === 'copy' || entry.mode === 'link');
181
+ const patterns = [];
182
+ for (const entry of entries) {
183
+ if (entry.mode === 'copy' || entry.mode === 'link') continue;
184
+ if (entry.mode === 'pattern' && entry.pattern) patterns.push(entry.pattern);
185
+ else warnings.push(`include-invalid:${entry.line}`);
186
+ }
187
+ const matched = patterns.length ? await ignoredPatternMatches(repoRoot, patterns, warnings) : [];
188
+ const overlaps = (a, b) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
189
+ const covered = (rel) => explicit.some((entry) => overlaps(relKey(rel), relKey(entry.rel)));
190
+ const planned = [...explicit];
191
+ for (const path of matched) {
192
+ let rel;
193
+ try {
194
+ rel = validateIncludePath(path);
195
+ } catch {
196
+ warnings.push(`include-skipped:${path}`);
197
+ continue;
198
+ }
199
+ if (!covered(rel)) planned.push({ mode: 'copy', rel, line: 0, pattern: true });
200
+ }
201
+ for (const entry of planned) {
202
+ try {
203
+ copyBytes += await planEntry(entry);
204
+ } catch (error) {
205
+ // An entry that breaks a rule is skipped (never copied or linked), never a
206
+ // failure; an I/O failure of an explicit entry still is one.
207
+ if (!entry.pattern && !isWorktreeError(error, 'WORKTREE_UNSUPPORTED_REPO')) throw error;
208
+ warnings.push(`include-skipped:${entry.rel}`);
209
+ }
210
+ }
211
+ return { items, copyBytes, warnings };
212
+
213
+ async function planEntry(entry) {
214
+ const source = join(rootReal, ...entry.rel.split('/'));
215
+ if (!(await exists(source))) {
216
+ warnings.push(`include-missing:${entry.rel}`);
217
+ return 0;
218
+ }
219
+ const sourceReal = await realpath(source);
220
+ if (!isWithin(rootReal, sourceReal) || sourceReal === rootReal) {
221
+ throw invalid(entry.line, `"${entry.rel}" resolves outside the repository`);
222
+ }
223
+ const realParts = relative(rootReal, sourceReal).split(sep);
224
+ if (realParts.some((part) => part.toLowerCase() === '.git')) {
225
+ throw invalid(entry.line, `"${entry.rel}" resolves into .git`);
226
+ }
227
+ if ((await insideUnityLibrary(rootReal, entry.rel.split('/'))) || (await insideUnityLibrary(rootReal, realParts))) {
228
+ throw invalid(entry.line, `"${entry.rel}" is a Unity Library folder, which is never copied or linked`);
229
+ }
230
+ const info = await stat(sourceReal);
231
+ if (entry.mode === 'link' && info.isDirectory() && (await isUnityProject(sourceReal))) {
232
+ throw invalid(entry.line, `"${entry.rel}" is a Unity project; linking it would share its Library folder`);
233
+ }
234
+ const bytes = entry.mode === 'copy' ? await treeBytes(sourceReal) : 0;
235
+ items.push({ mode: entry.mode, rel: entry.rel, source: sourceReal, directory: info.isDirectory(), bytes });
236
+ return bytes;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Untracked paths under `repoRoot` that match `patterns` (gitignore syntax,
242
+ * in order, so `!` negates) AND that the repository itself git-ignores;
243
+ * a fully matched directory is one entry. Git does the matching, so the
244
+ * semantics are exactly .gitignore's. A git failure is a warning.
245
+ */
246
+ async function ignoredPatternMatches(repoRoot, patterns, warnings) {
247
+ const excludes = patterns.map((pattern) => `--exclude=${pattern}`);
248
+ // Untracked paths the patterns select; `--directory` names a directory
249
+ // whose whole untracked content matches once instead of file by file.
250
+ const list = async (args) => {
251
+ const listed = await runGit(repoRoot, ['ls-files', '-z', '--others', '--ignored', ...args], {
252
+ allowFailure: true,
253
+ readOnly: true,
254
+ });
255
+ if (listed.code !== 0) throw new Error('ls-files failed');
256
+ return listed.stdout.split('\0').filter(Boolean);
257
+ };
258
+ // The subset the repository itself ignores (exit 1: none of them).
259
+ const repoIgnored = async (paths) => {
260
+ const ignored = new Set();
261
+ for (let index = 0; index < paths.length; index += CHECK_IGNORE_BATCH) {
262
+ const batch = paths.slice(index, index + CHECK_IGNORE_BATCH);
263
+ const result = await runGit(repoRoot, ['-c', 'core.quotePath=false', 'check-ignore', '--', ...batch], {
264
+ allowFailure: true,
265
+ readOnly: true,
266
+ });
267
+ if (result.code > 1) throw new Error('check-ignore failed');
268
+ for (const line of result.stdout.split(/\r?\n/)) if (line) ignored.add(line);
269
+ }
270
+ return ignored;
271
+ };
272
+ const selected = [];
273
+ try {
274
+ const candidates = await list(['--directory', '--no-empty-directory', ...excludes]);
275
+ const ignored = await repoIgnored(candidates);
276
+ for (const path of candidates) {
277
+ if (ignored.has(path)) {
278
+ selected.push(path);
279
+ continue;
280
+ }
281
+ // A directory the repository does not ignore as a whole: its files one by one.
282
+ if (!path.endsWith('/')) continue;
283
+ const files = await list([...excludes, '--', path]);
284
+ const inside = await repoIgnored(files);
285
+ selected.push(...files.filter((file) => inside.has(file)));
286
+ if (selected.length > MAX_PATTERN_MATCHES) break;
287
+ }
288
+ } catch {
289
+ warnings.push('include-invalid:patterns');
290
+ return [];
291
+ }
292
+ if (selected.length > MAX_PATTERN_MATCHES) {
293
+ // A partial copy would be worse than none.
294
+ warnings.push(`include-truncated:${selected.length}`);
295
+ return [];
296
+ }
297
+ return selected.map((path) => path.replace(/[\\/]+$/, ''));
298
+ }
299
+
300
+ /** Digest type letter of an lstat result: link, directory or file. */
301
+ function digestType(info) {
302
+ if (info.isSymbolicLink()) return 'l';
303
+ return info.isDirectory() ? 'd' : 'f';
304
+ }
305
+
306
+ /**
307
+ * Digest of a copied tree (relative path, type, size, mtime). Null when the
308
+ * tree is gone or too large to fingerprint.
309
+ */
310
+ async function treeDigest(path) {
311
+ const hash = createHash('sha256');
312
+ let count = 0;
313
+ async function walk(current, rel) {
314
+ const info = await lstat(current);
315
+ if (++count > MAX_DIGEST_ENTRIES) throw new RangeError('too many entries');
316
+ const type = digestType(info);
317
+ hash.update(`${rel}\0${type}\0${type === 'f' ? info.size : 0}\0${type === 'f' ? Math.trunc(info.mtimeMs) : 0}\n`);
318
+ if (type !== 'd') return;
319
+ for (const name of (await readdir(current)).sort()) await walk(join(current, name), rel ? `${rel}/${name}` : name);
320
+ }
321
+ try {
322
+ await walk(path, '');
323
+ } catch {
324
+ return null;
325
+ }
326
+ return hash.digest('hex');
327
+ }
328
+
329
+ /** Refuse a destination whose existing ancestors inside the worktree are links. */
330
+ async function assertPlainParents(worktreePath, dest) {
331
+ const parts = relative(worktreePath, dirname(dest)).split(sep).filter(Boolean);
332
+ let current = worktreePath;
333
+ for (const part of parts) {
334
+ current = join(current, part);
335
+ let info;
336
+ try {
337
+ info = await lstat(current);
338
+ } catch {
339
+ return;
340
+ }
341
+ if (info.isSymbolicLink() || !info.isDirectory()) {
342
+ throw worktreeError(
343
+ 'WORKTREE_UNSUPPORTED_REPO',
344
+ `${INCLUDE_FILE}: ${relative(worktreePath, dest)} would pass through a link.`
345
+ );
346
+ }
347
+ }
348
+ }
349
+
350
+ async function makeLink(item, dest) {
351
+ const failures = [];
352
+ if (item.directory) {
353
+ try {
354
+ await symlink(item.source, dest, process.platform === 'win32' ? 'junction' : 'dir');
355
+ return;
356
+ } catch (error) {
357
+ failures.push(error);
358
+ }
359
+ } else {
360
+ try {
361
+ await symlink(item.source, dest, 'file');
362
+ return;
363
+ } catch (error) {
364
+ failures.push(error);
365
+ }
366
+ if (process.platform === 'win32') {
367
+ try {
368
+ await link(item.source, dest);
369
+ return;
370
+ } catch (error) {
371
+ failures.push(error);
372
+ }
373
+ }
374
+ }
375
+ throw worktreeError(
376
+ 'WORKTREE_UNSUPPORTED_REPO',
377
+ `${INCLUDE_FILE}: could not link ${item.rel} (${failures.map((error) => error.code || error.message).join(', ')}). ` +
378
+ 'Links are never replaced by copies; use "copy" or enable symbolic links.',
379
+ { rel: item.rel }
380
+ );
381
+ }
382
+
383
+ /**
384
+ * Copy/link every planned item into `worktreePath`. `record(item)` is awaited
385
+ * with status 'pending' before each resource is made and 'created' after, so
386
+ * a rollback knows exactly what this transaction produced.
387
+ */
388
+ export async function applyIncludes(plan, worktreePath, record) {
389
+ const warnings = [];
390
+ for (const item of plan.items) {
391
+ const dest = join(worktreePath, ...item.rel.split('/'));
392
+ if (await exists(dest)) {
393
+ warnings.push(`include-exists:${item.rel}`);
394
+ continue;
395
+ }
396
+ try {
397
+ await assertPlainParents(worktreePath, dest);
398
+ } catch (error) {
399
+ if (!isWorktreeError(error, 'WORKTREE_UNSUPPORTED_REPO')) throw error;
400
+ // The destination would pass through a link: skipped, never written.
401
+ warnings.push(`include-skipped:${item.rel}`);
402
+ continue;
403
+ }
404
+ const ledger = { mode: item.mode, rel: item.rel, path: dest, status: 'pending', digest: null };
405
+ await record(ledger);
406
+ await mkdir(dirname(dest), { recursive: true });
407
+ if (item.mode === 'link') {
408
+ await makeLink(item, dest);
409
+ } else if (item.directory) {
410
+ await cp(item.source, dest, {
411
+ recursive: true,
412
+ force: false,
413
+ errorOnExist: true,
414
+ verbatimSymlinks: true,
415
+ filter: async (source) => !(await isUnityLibraryDir(source)),
416
+ });
417
+ } else {
418
+ await copyFile(item.source, dest, fsConstants.COPYFILE_EXCL);
419
+ }
420
+ ledger.status = 'created';
421
+ if (item.mode === 'copy') ledger.digest = await treeDigest(dest);
422
+ await record(ledger);
423
+ }
424
+ return { warnings };
425
+ }
426
+
427
+ /**
428
+ * Remove the links this worktree made, never traversing their targets.
429
+ * Copies stay (they live inside the checkout, removed with it).
430
+ * @returns {Promise<string[]>} Links that could not be removed.
431
+ */
432
+ export async function removeIncludeLinks(items) {
433
+ const remaining = [];
434
+ for (const item of items || []) {
435
+ if (item.mode !== 'link') continue;
436
+ let info;
437
+ try {
438
+ info = await lstat(item.path);
439
+ } catch {
440
+ continue;
441
+ }
442
+ // A hard-linked file (Windows fallback) is a regular file; unlinking it
443
+ // drops only this name.
444
+ if (!info.isSymbolicLink() && !info.isFile()) continue;
445
+ try {
446
+ await unlink(item.path);
447
+ } catch (error) {
448
+ if (!['EPERM', 'EISDIR', 'EACCES'].includes(error?.code)) {
449
+ remaining.push(item.path);
450
+ continue;
451
+ }
452
+ try {
453
+ await rmdir(item.path);
454
+ } catch {
455
+ remaining.push(item.path);
456
+ }
457
+ }
458
+ }
459
+ return remaining;
460
+ }
461
+
462
+ /** Copied include paths whose content changed since creation (or unknown). */
463
+ export async function modifiedIncludeCopies(items) {
464
+ const modified = [];
465
+ for (const item of items || []) {
466
+ if (item.mode !== 'copy' || !(await exists(item.path))) continue;
467
+ if (item.status !== 'created' || !item.digest || (await treeDigest(item.path)) !== item.digest)
468
+ modified.push(item.rel);
469
+ }
470
+ return modified;
471
+ }
472
+
473
+ /** True when `path` (forward-slash, relative) is one of the include items or inside one. */
474
+ export function coveredByIncludes(items, path) {
475
+ const target = relKey(String(path).replace(/\\/g, '/').replace(/\/$/, ''));
476
+ return (items || []).some((item) => {
477
+ const base = relKey(item.rel);
478
+ return target === base || target.startsWith(`${base}/`);
479
+ });
480
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Repository mutation lock for managed worktree operations (create, checkout,
3
+ * merge, remove, reconcile). Keyed by the canonical git common dir so the
4
+ * main checkout and every linked worktree serialize:
5
+ * - in-process: the git tool's repository read/write lock (write side), so
6
+ * agent git calls never interleave with a managed mutation;
7
+ * - cross-process: a file lock under <dataDir>/worktrees-v1/<repoHash>.lock,
8
+ * shared by the runtime daemon and the desktop main process.
9
+ * Re-entrant within one async call chain.
10
+ */
11
+ import { AsyncLocalStorage } from 'node:async_hooks';
12
+ import { join } from 'node:path';
13
+ import { withGitRepoWriteLock } from '../../agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs';
14
+ import { withFileLock } from '../file-lock.mjs';
15
+ import { resolvePluginData } from '../plugin-paths.mjs';
16
+ import { repoHash, WORKTREE_STORE_DIR } from './contract.mjs';
17
+ import { canonicalPath, pathKey } from './paths.mjs';
18
+
19
+ const REPO_LOCK_TIMEOUT_MS = 120_000;
20
+ const heldRepoLocks = new AsyncLocalStorage();
21
+
22
+ /** Cross-process lock file for one repository (outside the removable root). */
23
+ export function repoLockPath(repoCommonDir, { dataDir = resolvePluginData() } = {}) {
24
+ return join(dataDir, WORKTREE_STORE_DIR, `${repoHash(canonicalPath(repoCommonDir))}.lock`);
25
+ }
26
+
27
+ /**
28
+ * Run `fn` holding the repository's in-process and cross-process locks.
29
+ * @template T
30
+ * @param {string} repoCommonDir
31
+ * @param {() => Promise<T>|T} fn
32
+ * @param {{ signal?: AbortSignal, timeoutMs?: number }} [options]
33
+ * @returns {Promise<T>}
34
+ */
35
+ export async function withRepoLock(repoCommonDir, fn, { signal, timeoutMs = REPO_LOCK_TIMEOUT_MS } = {}) {
36
+ const key = pathKey(repoCommonDir);
37
+ const held = heldRepoLocks.getStore();
38
+ if (held?.has(key)) return fn();
39
+ const next = new Set(held || []);
40
+ next.add(key);
41
+ return withGitRepoWriteLock(
42
+ key,
43
+ () => withFileLock(repoLockPath(repoCommonDir), () => heldRepoLocks.run(next, fn), { timeoutMs }),
44
+ { signal }
45
+ );
46
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Session-aware worktree maintenance: the daemon-startup reconcile, the
3
+ * manual list/prune (Lead tool, desktop) and the cleanup after a session is
4
+ * deleted. A worktree whose session is still in the session store is never
5
+ * removed by any of them; only explicit removal (removeWorktree) does that.
6
+ */
7
+ import { readdir } from 'node:fs/promises';
8
+ import { isWorktreeError } from './contract.mjs';
9
+
10
+ const loadManager = () => import('./manager.mjs');
11
+
12
+ /**
13
+ * Ids of every session record in the store (one `<id>.json` per session,
14
+ * tombstones included). An unreadable store throws: it never reads as
15
+ * "no sessions", which would make every worktree look orphaned.
16
+ */
17
+ export async function storedSessionIds() {
18
+ const { getStoreDir } = await import('../../agent/orchestrator/session/store/paths-heartbeat.mjs');
19
+ const names = await readdir(getStoreDir());
20
+ return names
21
+ .filter((name) => name.endsWith('.json'))
22
+ .map((name) => name.slice(0, -5))
23
+ .filter((id) => /^[A-Za-z0-9_-]+$/.test(id));
24
+ }
25
+
26
+ /**
27
+ * Daemon startup: resume interrupted deletions and creations, release stale
28
+ * leases, prune stale registrations; with autoCleanup on, also remove clean,
29
+ * merged worktrees no stored session retains.
30
+ * @param {{ liveSessionIds?: string[], listSessionIds?: () => Promise<string[]>, manager?: object }} [options]
31
+ */
32
+ export async function reconcileWorktreesAtStartup({
33
+ liveSessionIds = [],
34
+ listSessionIds = storedSessionIds,
35
+ manager = null,
36
+ } = {}) {
37
+ const backend = manager || (await loadManager());
38
+ const settings = await backend.currentWorktreeSettings();
39
+ return backend.reconcileManagedWorktrees({
40
+ liveSessionIds,
41
+ retainedSessionIds: await listSessionIds(),
42
+ removeOrphans: settings.autoCleanup === true,
43
+ });
44
+ }
45
+
46
+ /**
47
+ * The manual list: every managed worktree with its retention.
48
+ * @param {{ liveSessionIds?: string[], listSessionIds?: () => Promise<string[]>, manager?: object }} [options]
49
+ */
50
+ export async function listWorktreesWithRetention({
51
+ liveSessionIds = [],
52
+ listSessionIds = storedSessionIds,
53
+ manager = null,
54
+ } = {}) {
55
+ const backend = manager || (await loadManager());
56
+ return backend.listManagedWorktrees({ liveSessionIds, retainedSessionIds: await listSessionIds() });
57
+ }
58
+
59
+ /**
60
+ * The manual prune (an explicit request, so independent of autoCleanup):
61
+ * the startup reconcile plus removal of clean, merged worktrees that no
62
+ * stored or live session retains. Dirty or unmerged ones are reported kept.
63
+ * @param {{ liveSessionIds?: string[], listSessionIds?: () => Promise<string[]>, manager?: object }} [options]
64
+ */
65
+ export async function pruneWorktrees({ liveSessionIds = [], listSessionIds = storedSessionIds, manager = null } = {}) {
66
+ const backend = manager || (await loadManager());
67
+ return backend.reconcileManagedWorktrees({
68
+ liveSessionIds,
69
+ retainedSessionIds: await listSessionIds(),
70
+ removeOrphans: true,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * After a session was deleted: with autoCleanup on, its worktree is marked
76
+ * disposable and removed when clean and merged (dirty or unmerged work is
77
+ * kept, still disposable, for a later prune once it is clean). Off: kept.
78
+ * @param {string} sessionId
79
+ * @param {{ manager?: object, autoCleanup?: boolean }} [options]
80
+ */
81
+ export async function cleanupDeletedSessionWorktree(sessionId, { manager = null, autoCleanup } = {}) {
82
+ const backend = manager || (await loadManager());
83
+ const entry = await backend.getSessionWorktree(sessionId);
84
+ if (!entry) return null;
85
+ const enabled = autoCleanup ?? (await backend.currentWorktreeSettings()).autoCleanup;
86
+ if (enabled !== true) return { entryId: entry.id, removed: false, reason: 'auto-cleanup-off' };
87
+ if (entry.state === 'ready') await backend.markWorktreeDisposable(entry.id);
88
+ try {
89
+ return { entryId: entry.id, ...(await backend.removeManagedWorktree({ entryId: entry.id })) };
90
+ } catch (error) {
91
+ if (isWorktreeError(error, 'WORKTREE_DIRTY') || isWorktreeError(error, 'WORKTREE_UNMERGED')) {
92
+ return { entryId: entry.id, removed: false, reason: error.code };
93
+ }
94
+ throw error;
95
+ }
96
+ }