mixdog 1.1.1 → 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 (317) hide show
  1. package/README.md +1 -0
  2. package/package.json +2 -2
  3. package/scripts/lib/run-node-tests.mjs +9 -0
  4. package/scripts/prune-desktop-runtime.mjs +3 -0
  5. package/scripts/release-paths.mjs +39 -2
  6. package/scripts/test-direct.mjs +4 -2
  7. package/src/defaults/shipped-history.json +6 -0
  8. package/src/defaults/skills/setup/references/actions.md +5 -2
  9. package/src/defaults/skills/visualize/SKILL.md +157 -0
  10. package/src/defaults/skills/visualize/references/craft.md +88 -0
  11. package/src/headless-command.mjs +56 -3
  12. package/src/headless-exec.mjs +17 -1
  13. package/src/headless-role.mjs +2 -2
  14. package/src/help.mjs +2 -0
  15. package/src/lib/rules-builder.cjs +0 -23
  16. package/src/rules/lead/LEAD.md +2 -2
  17. package/src/rules/shared/70-delivery.md +2 -2
  18. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  19. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  21. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  22. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  23. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  24. package/src/runtime/agent/orchestrator/config.mjs +9 -1
  25. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
  26. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
  27. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
  28. package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
  29. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  35. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
  37. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  38. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  40. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  41. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  43. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  44. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
  45. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
  46. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  47. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  48. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  49. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  51. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  53. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  57. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
  58. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  59. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +11 -8
  61. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  62. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  63. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  64. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  65. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
  66. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  67. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  68. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  69. package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
  70. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  72. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  74. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
  75. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
  76. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  82. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  83. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  84. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  89. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  91. package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
  92. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  93. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  94. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
  95. package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
  96. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  97. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  98. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  99. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
  100. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
  101. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
  102. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  103. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
  104. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  106. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  107. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  111. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  112. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  113. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
  114. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  115. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  117. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  118. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  119. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  121. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  123. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  124. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  125. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  126. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +0 -10
  128. package/src/runtime/attachments/store.mjs +38 -0
  129. package/src/runtime/channels/index.mjs +1 -5
  130. package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
  131. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
  132. package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
  133. package/src/runtime/channels/lib/worker-main.mjs +4 -34
  134. package/src/runtime/shared/atomic-file.mjs +8 -2
  135. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  136. package/src/runtime/shared/code-graph-state.mjs +12 -0
  137. package/src/runtime/shared/defaults-separation.mjs +2 -0
  138. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  139. package/src/runtime/shared/git-runner.mjs +4 -3
  140. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  141. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  142. package/src/runtime/shared/llm/model-catalog.mjs +240 -118
  143. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  144. package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
  145. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  146. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  147. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  148. package/src/runtime/shared/provider-accounts.mjs +2 -2
  149. package/src/runtime/shared/provider-api-key.mjs +21 -1
  150. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  151. package/src/runtime/shared/semver.d.mts +1 -0
  152. package/src/runtime/shared/semver.mjs +57 -0
  153. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  154. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  155. package/src/runtime/shared/singleton-owner.mjs +36 -6
  156. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  157. package/src/runtime/shared/tool-surface.mjs +9 -2
  158. package/src/runtime/shared/tool-work-units.mjs +14 -0
  159. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  160. package/src/runtime/shared/turn-changes.mjs +791 -0
  161. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  162. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  163. package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
  164. package/src/runtime/shared/update-checker.mjs +2 -31
  165. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  166. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  167. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  168. package/src/runtime/shared/worktrees/git.mjs +247 -0
  169. package/src/runtime/shared/worktrees/include.mjs +480 -0
  170. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  171. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  172. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  173. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  174. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  175. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  176. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  177. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  178. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  179. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  180. package/src/runtime/visualize-bridge/client.mjs +181 -0
  181. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  182. package/src/session-runtime/boot/apis.mjs +35 -0
  183. package/src/session-runtime/boot/begin.mjs +0 -2
  184. package/src/session-runtime/boot/config.mjs +2 -0
  185. package/src/session-runtime/boot/facade.mjs +20 -0
  186. package/src/session-runtime/boot/infrastructure.mjs +1 -0
  187. package/src/session-runtime/boot/session.mjs +1 -0
  188. package/src/session-runtime/boot/tools.mjs +9 -4
  189. package/src/session-runtime/config-lifecycle.mjs +10 -1
  190. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
  191. package/src/session-runtime/global-extensions.mjs +2 -2
  192. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  193. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  194. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  195. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  196. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  197. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  198. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  199. package/src/session-runtime/mcp-glue/connect.mjs +11 -6
  200. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
  201. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  202. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  203. package/src/session-runtime/model-route/set-route.mjs +15 -2
  204. package/src/session-runtime/notification-bus.mjs +9 -1
  205. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  206. package/src/session-runtime/resource-extension-sync.mjs +30 -7
  207. package/src/session-runtime/resource-mcp-api.mjs +27 -10
  208. package/src/session-runtime/route-resolution.mjs +10 -1
  209. package/src/session-runtime/route-state.mjs +16 -2
  210. package/src/session-runtime/runtime-bootstrap.mjs +0 -4
  211. package/src/session-runtime/runtime-core.mjs +5 -0
  212. package/src/session-runtime/runtime-facade.mjs +8 -0
  213. package/src/session-runtime/runtime-paths.mjs +0 -1
  214. package/src/session-runtime/runtime-review-api.mjs +9 -0
  215. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  216. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  217. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  218. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  219. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  220. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  221. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  222. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  223. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  224. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  225. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  226. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  227. package/src/session-runtime/services/agent-tool.mjs +1 -0
  228. package/src/session-runtime/services/channel-worker.mjs +1 -1
  229. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  230. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  231. package/src/session-runtime/session-create/session-options.mjs +10 -1
  232. package/src/session-runtime/session-turn-api.mjs +1 -0
  233. package/src/session-runtime/settings-api.mjs +4 -2
  234. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  235. package/src/session-runtime/settings-profile-api.mjs +27 -2
  236. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  237. package/src/session-runtime/setup-tool/executor.mjs +11 -4
  238. package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
  239. package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
  240. package/src/session-runtime/starter-guide-state.mjs +62 -0
  241. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  242. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
  243. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  244. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  245. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  246. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  247. package/src/session-runtime/turn/turn-run.mjs +6 -7
  248. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  249. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
  250. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  251. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  252. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  253. package/src/session-runtime/workspace/transitions.mjs +651 -0
  254. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  255. package/src/standalone/channel-binding.mjs +1 -2
  256. package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
  257. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  258. package/src/standalone/channel-transport.mjs +13 -1
  259. package/src/standalone/daemon-channels-loader.mjs +2 -3
  260. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  261. package/src/standalone/daemon-owner.mjs +372 -0
  262. package/src/standalone/daemon-stored-session-views.mjs +4 -1
  263. package/src/standalone/daemon.mjs +139 -5
  264. package/src/standalone/desktop-service-registry.mjs +20 -0
  265. package/src/standalone/local-session-runtime.mjs +13 -78
  266. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  267. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  268. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  269. package/src/standalone/session-client.mjs +104 -13
  270. package/src/standalone/session-protocol.mjs +17 -0
  271. package/src/standalone/session-proxy.mjs +3 -0
  272. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  273. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  274. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  275. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  276. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  277. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  278. package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
  279. package/src/standalone/session-transport/call-route.mjs +13 -0
  280. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  281. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  282. package/src/standalone/session-transport/client-registry.mjs +20 -3
  283. package/src/standalone/session-transport/routes.mjs +79 -2
  284. package/src/standalone/session-transport/stack.mjs +2 -0
  285. package/src/standalone/session-transport.mjs +24 -1
  286. package/src/tui/app/message-selector.mjs +2 -1
  287. package/src/tui/app/model-options.mjs +35 -5
  288. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  289. package/src/tui/app/model-picker.mjs +4 -2
  290. package/src/tui/dist/index.mjs +332 -36
  291. package/src/tui/index.jsx +10 -2
  292. package/src/tui/session/compaction-view.mjs +147 -0
  293. package/src/tui/session/context-state.mjs +6 -0
  294. package/src/tui/session/initial-state.mjs +3 -0
  295. package/src/tui/session/quota-resume.mjs +2 -1
  296. package/src/tui/session/session-api/commands.mjs +10 -2
  297. package/src/tui/session/session-api/controls.mjs +4 -0
  298. package/src/tui/session/session-api/extensions.mjs +12 -8
  299. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  300. package/src/tui/session/session-api/integrations.mjs +18 -0
  301. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  302. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  303. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  304. package/src/tui/session/session-api/routes.mjs +10 -1
  305. package/src/tui/session/session-api/settings.mjs +2 -0
  306. package/src/tui/session/session-api/shared.mjs +10 -6
  307. package/src/tui/session/session-api/worktree.mjs +60 -0
  308. package/src/tui/session/session-api.mjs +2 -0
  309. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  310. package/src/tui/session/session-flow/drain.mjs +15 -1
  311. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  312. package/src/tui/session/session-flow.mjs +2 -0
  313. package/src/tui/session/transcript-intake.mjs +14 -5
  314. package/src/tui/session/turn-stream.mjs +32 -4
  315. package/src/tui/session/turn-tool-cards.mjs +10 -2
  316. package/src/tui/session/turn.mjs +39 -10
  317. package/src/runtime/channels/tool-defs.mjs +0 -17
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Unified merge backend: fold a branch into the branch checked out in a
3
+ * project folder. Shared by the managed worktree merge and the desktop
4
+ * Source Control dock, so both have one semantics:
5
+ * - explicit confirmation is required;
6
+ * - the folder must be on `target` (when given), with no operation in
7
+ * progress and no uncommitted or untracked changes;
8
+ * - `git merge-tree --write-tree` previews the exact commits first, and a
9
+ * conflicting merge returns its files WITHOUT touching the folder;
10
+ * - a clean preview merges with --no-ff --no-edit; if that merge still
11
+ * fails (hook, interruption) only the merge this call started is aborted
12
+ * and the failure is thrown, never reported as success.
13
+ * The caller holds the repository lock (lock.mjs) around the whole call.
14
+ *
15
+ * `git(cwd, args)` resolves to { code, stdout, stderr } and never throws on a
16
+ * non-zero exit.
17
+ */
18
+ import { existsSync } from 'node:fs';
19
+ import { resolve } from 'node:path';
20
+ import { worktreeError } from './contract.mjs';
21
+ import { firstLine, parseStatusPaths } from './git.mjs';
22
+
23
+ function failure(args, result) {
24
+ const detail = String(result?.stderr || result?.stdout || '').trim() || 'no diagnostic';
25
+ const error = new Error(`git ${args[0]} failed (exit ${result?.code}): ${detail}`);
26
+ error.code = 'GIT_FAILED';
27
+ return error;
28
+ }
29
+
30
+ /** The sequencer operation in progress in `cwd`, or ''. */
31
+ export async function operationInProgress(git, cwd) {
32
+ for (const [ref, name] of [
33
+ ['MERGE_HEAD', 'merge'],
34
+ ['CHERRY_PICK_HEAD', 'cherry-pick'],
35
+ ['REVERT_HEAD', 'revert'],
36
+ ]) {
37
+ if ((await git(cwd, ['rev-parse', '--verify', '--quiet', ref])).code === 0) return name;
38
+ }
39
+ const paths = await git(cwd, [
40
+ 'rev-parse',
41
+ '--path-format=absolute',
42
+ '--git-path',
43
+ 'rebase-merge',
44
+ '--git-path',
45
+ 'rebase-apply',
46
+ ]);
47
+ if (paths.code === 0) {
48
+ for (const path of String(paths.stdout)
49
+ .split(/\r?\n/)
50
+ .map((line) => line.trim())
51
+ .filter(Boolean)) {
52
+ if (existsSync(resolve(cwd, path))) return 'rebase';
53
+ }
54
+ }
55
+ return '';
56
+ }
57
+
58
+ /** Changed and untracked paths in `cwd` (porcelain v1, -z). */
59
+ export async function uncommittedPaths(git, cwd) {
60
+ const args = ['status', '--porcelain=v1', '-z', '--untracked-files=normal'];
61
+ const result = await git(cwd, args);
62
+ if (result.code !== 0) throw failure(args, result);
63
+ return parseStatusPaths(result.stdout);
64
+ }
65
+
66
+ async function revParse(git, cwd, rev) {
67
+ const result = await git(cwd, ['rev-parse', '--verify', '--quiet', rev]);
68
+ return result.code === 0 ? firstLine(result.stdout) : null;
69
+ }
70
+
71
+ async function checkedOutBranch(git, cwd) {
72
+ const result = await git(cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
73
+ return result.code === 0 ? firstLine(result.stdout) || null : null;
74
+ }
75
+
76
+ /**
77
+ * @typedef {{ merged: boolean, commit: string|null, target: string, conflicts?: string[], upToDate?: boolean }} MergeResult
78
+ */
79
+
80
+ /**
81
+ * @param {{
82
+ * git: (cwd: string, args: string[]) => Promise<{ code: number, stdout: string, stderr: string }>,
83
+ * cwd: string,
84
+ * source: string,
85
+ * target?: string|null,
86
+ * confirmed: boolean,
87
+ * message?: string,
88
+ * }} input
89
+ * @returns {Promise<MergeResult>}
90
+ */
91
+ export async function mergeBranchIntoCheckout({ git, cwd, source, target = null, confirmed, message }) {
92
+ if (confirmed !== true) throw worktreeError('WORKTREE_CONFIRM_REQUIRED');
93
+ const sourceName = String(source || '').trim();
94
+ if (!sourceName || sourceName.startsWith('-')) {
95
+ throw worktreeError('MERGE_FAILED', 'A merge source branch is required.', { reason: 'source-invalid' });
96
+ }
97
+ const current = await checkedOutBranch(git, cwd);
98
+ if (target !== null && target !== undefined && target !== current) {
99
+ throw worktreeError(
100
+ 'WORKTREE_TARGET_MISMATCH',
101
+ `The project folder is on ${current ?? 'a detached HEAD'}, not ${target}; reload and try again.`,
102
+ { expected: target, actual: current }
103
+ );
104
+ }
105
+ if (!current) {
106
+ throw worktreeError('MERGE_FAILED', 'Check out a branch in the project folder before merging.', {
107
+ reason: 'detached-head',
108
+ });
109
+ }
110
+ const operation = await operationInProgress(git, cwd);
111
+ if (operation) {
112
+ throw worktreeError('MERGE_TARGET_DIRTY', `A ${operation} is already in progress in the project folder.`, {
113
+ operation,
114
+ files: [],
115
+ });
116
+ }
117
+ const dirty = await uncommittedPaths(git, cwd);
118
+ if (dirty.length) {
119
+ throw worktreeError('MERGE_TARGET_DIRTY', undefined, {
120
+ operation: '',
121
+ files: dirty.slice(0, 50),
122
+ count: dirty.length,
123
+ });
124
+ }
125
+ const head = await revParse(git, cwd, 'HEAD^{commit}');
126
+ if (!head) throw worktreeError('MERGE_FAILED', 'The merge target has no commits.', { reason: 'target-empty' });
127
+ const sourceCommit = await revParse(git, cwd, `${sourceName}^{commit}`);
128
+ if (!sourceCommit) {
129
+ throw worktreeError('MERGE_FAILED', `No branch or commit named ${sourceName}.`, {
130
+ reason: 'source-missing',
131
+ source: sourceName,
132
+ });
133
+ }
134
+
135
+ if ((await git(cwd, ['merge-base', '--is-ancestor', sourceCommit, head])).code === 0) {
136
+ return { merged: true, commit: head, target: current, upToDate: true };
137
+ }
138
+ const previewArgs = ['merge-tree', '--write-tree', '--name-only', '--no-messages', '-z', head, sourceCommit];
139
+ const preview = await git(cwd, previewArgs);
140
+ if (preview.code === 1) {
141
+ const conflicts = [...new Set(String(preview.stdout).split('\0').slice(1).filter(Boolean))];
142
+ return { merged: false, commit: null, target: current, conflicts };
143
+ }
144
+ if (preview.code !== 0) throw failure(previewArgs, preview);
145
+
146
+ // Nothing Mixdog runs can move the folder while the caller holds the
147
+ // repository lock; an outside process still could, so re-verify.
148
+ if ((await revParse(git, cwd, 'HEAD^{commit}')) !== head || (await checkedOutBranch(git, cwd)) !== current) {
149
+ throw worktreeError('WORKTREE_TARGET_MISMATCH', 'The project folder changed during the merge; try again.', {
150
+ expected: current,
151
+ });
152
+ }
153
+ const mergeArgs = [
154
+ 'merge',
155
+ '--no-ff',
156
+ '--no-edit',
157
+ '-m',
158
+ message || `Merge branch '${sourceName}' into ${current}`,
159
+ sourceCommit,
160
+ ];
161
+ const merged = await git(cwd, mergeArgs);
162
+ if (merged.code !== 0) {
163
+ // No operation was in progress before this call, so a MERGE_HEAD now is
164
+ // the merge this call started.
165
+ let aborted = false;
166
+ if (await revParse(git, cwd, 'MERGE_HEAD')) aborted = (await git(cwd, ['merge', '--abort'])).code === 0;
167
+ const outcome = aborted ? ' (the merge was aborted; the project folder is unchanged)' : '';
168
+ const error = worktreeError('MERGE_FAILED', `${failure(mergeArgs, merged).message}${outcome}`, {
169
+ reason: 'merge-failed',
170
+ aborted,
171
+ });
172
+ error.aborted = aborted;
173
+ throw error;
174
+ }
175
+ const commit = await revParse(git, cwd, 'HEAD^{commit}');
176
+ if (!commit || commit === head || (await revParse(git, cwd, 'HEAD^2')) !== sourceCommit) {
177
+ throw worktreeError('MERGE_FAILED', 'git merge reported success but HEAD is not the expected merge commit.', {
178
+ reason: 'unexpected-head',
179
+ });
180
+ }
181
+ return { merged: true, commit, target: current };
182
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Live owner leases. Durable ownership is the registry record's
3
+ * `ownerSessionId` (the session retains the worktree across restarts); the
4
+ * lease says which runtime of that session holds it right now. A lease is
5
+ * matched on the exact owner (sessionId + runtimeToken); the pid is a
6
+ * diagnostic hint only, never the identity.
7
+ *
8
+ * Leases live at <root>/.owners/<id>.json (outside the worktree checkout, so
9
+ * they never show up as untracked files in it).
10
+ */
11
+ import { readFile, rm } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ import { parseJsonText, writeJsonAtomicAsync } from '../atomic-file.mjs';
14
+ import { managedWorktreeRoot, normalizeWorktreeOwner } from './contract.mjs';
15
+
16
+ function ownerLeasePath(record) {
17
+ return join(managedWorktreeRoot(record.repoCommonDir), '.owners', `${record.id}.json`);
18
+ }
19
+
20
+ export async function readOwnerLease(record) {
21
+ try {
22
+ const value = parseJsonText(await readFile(ownerLeasePath(record), 'utf8'));
23
+ return {
24
+ ...normalizeWorktreeOwner(value),
25
+ entryId: value.entryId ?? record.id,
26
+ claimedAt: value.claimedAt ?? null,
27
+ };
28
+ } catch (error) {
29
+ if (error?.code === 'ENOENT' || error instanceof SyntaxError || error instanceof TypeError) return null;
30
+ throw error;
31
+ }
32
+ }
33
+
34
+ export async function writeOwnerLease(record, owner) {
35
+ const lease = { ...normalizeWorktreeOwner(owner), entryId: record.id, claimedAt: Date.now() };
36
+ await writeJsonAtomicAsync(ownerLeasePath(record), lease);
37
+ return lease;
38
+ }
39
+
40
+ export function sameOwner(left, right) {
41
+ return !!left && !!right && left.sessionId === right.sessionId && left.runtimeToken === right.runtimeToken;
42
+ }
43
+
44
+ export async function removeOwnerLease(record) {
45
+ await rm(ownerLeasePath(record), { force: true });
46
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Canonical path helpers for managed worktrees. Git reports paths with forward
3
+ * slashes, long names and resolved symlinks while callers may hold 8.3 short
4
+ * names, other casing (Windows) or symlinked parents (macOS /var). Every
5
+ * comparison and every lock key goes through these.
6
+ */
7
+ import { existsSync, realpathSync } from 'node:fs';
8
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
9
+
10
+ /** `resolve` plus the real path of the nearest existing ancestor. */
11
+ export function canonicalPath(value) {
12
+ const full = resolve(String(value || '.'));
13
+ let head = full;
14
+ while (!existsSync(head)) {
15
+ const parent = dirname(head);
16
+ if (parent === head) return full;
17
+ head = parent;
18
+ }
19
+ let real;
20
+ try {
21
+ real = realpathSync.native(head);
22
+ } catch {
23
+ return full;
24
+ }
25
+ const rest = relative(head, full);
26
+ return rest ? join(real, rest) : real;
27
+ }
28
+
29
+ /** Comparison key: canonical, case-insensitive on Windows. */
30
+ export function pathKey(value, platform = process.platform) {
31
+ const path = canonicalPath(value).replace(/[\\/]+$/, '') || sep;
32
+ return platform === 'win32' ? path.toLowerCase() : path;
33
+ }
34
+
35
+ export function samePath(left, right) {
36
+ return pathKey(left) === pathKey(right);
37
+ }
38
+
39
+ /** True when `child` is `parent` itself or lies inside it (canonical). */
40
+ export function isWithin(parent, child) {
41
+ const base = pathKey(parent);
42
+ const target = pathKey(child);
43
+ return target === base || target.startsWith(base.endsWith(sep) ? base : base + sep);
44
+ }
45
+
46
+ /**
47
+ * Repository-relative folder of a Project registered below the repository
48
+ * root ('' for the root itself or anything outside it), with forward slashes.
49
+ * A session of that Project executes in the same folder of its worktree.
50
+ * @param {string|null|undefined} topLevel
51
+ * @param {string|null|undefined} projectPath
52
+ */
53
+ export function projectSubdirOf(topLevel, projectPath) {
54
+ if (!topLevel || !projectPath || !isWithin(topLevel, projectPath)) return '';
55
+ const rel = relative(canonicalPath(topLevel), canonicalPath(projectPath));
56
+ return !rel || rel.startsWith('..') || isAbsolute(rel) ? '' : rel.split(sep).join('/');
57
+ }
58
+
59
+ /** `subdir` (forward slashes, from projectSubdirOf) inside `root`. */
60
+ export function checkoutFolder(root, subdir) {
61
+ return subdir ? join(root, ...subdir.split('/')) : root;
62
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Durable registry of managed worktrees: <dataDir>/worktrees-v1/registry.json.
3
+ *
4
+ * One record per worktree, keyed by its slug (globally unique, so an entry id
5
+ * alone addresses it). A record carries the public entry fields plus the
6
+ * transaction ledger that makes creation rollback and deletion resumable:
7
+ * which resources this transaction created ('pending' is written before a
8
+ * resource is made, 'created' after), the deletion decision and any recovery
9
+ * diagnostics. Writes are atomic (temp file + rename) under a file lock; an
10
+ * unreadable registry is an error, never silently replaced.
11
+ */
12
+ import { readFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { parseJsonText, withFileLock, writeJsonAtomicAsync } from '../atomic-file.mjs';
15
+ import { isPlainObject } from '../object.mjs';
16
+ import { resolvePluginData } from '../plugin-paths.mjs';
17
+ import { normalizeWorktreeEntry, WORKTREE_STORE_DIR } from './contract.mjs';
18
+
19
+ const REGISTRY_VERSION = 1;
20
+
21
+ function worktreeStoreDir() {
22
+ return join(resolvePluginData(), WORKTREE_STORE_DIR);
23
+ }
24
+
25
+ export function registryPath() {
26
+ return join(worktreeStoreDir(), 'registry.json');
27
+ }
28
+
29
+ function emptyDoc() {
30
+ return { version: REGISTRY_VERSION, entries: {} };
31
+ }
32
+
33
+ async function readDoc(path) {
34
+ let text;
35
+ try {
36
+ text = await readFile(path, 'utf8');
37
+ } catch (error) {
38
+ if (error?.code === 'ENOENT') return emptyDoc();
39
+ throw error;
40
+ }
41
+ const value = parseJsonText(text);
42
+ if (!isPlainObject(value) || !isPlainObject(value.entries)) {
43
+ throw new Error(`Worktree registry ${path} is malformed.`);
44
+ }
45
+ return value;
46
+ }
47
+
48
+ /** Snapshot of every record (plain clones). */
49
+ export async function readRegistry() {
50
+ return readDoc(registryPath());
51
+ }
52
+
53
+ /**
54
+ * Locked read-modify-write. `mutate(doc)` edits doc.entries in place; its
55
+ * return value is returned.
56
+ * @template T
57
+ * @param {(doc: { version: number, entries: Record<string, any> }) => T} mutate
58
+ * @returns {Promise<T>}
59
+ */
60
+ export async function mutateRegistry(mutate) {
61
+ const path = registryPath();
62
+ return withFileLock(
63
+ `${path}.lock`,
64
+ async () => {
65
+ const doc = await readDoc(path);
66
+ const result = mutate(doc);
67
+ await writeJsonAtomicAsync(path, doc, { lock: false });
68
+ return result;
69
+ },
70
+ { timeoutMs: 15_000 }
71
+ );
72
+ }
73
+
74
+ export async function getRecord(id) {
75
+ const doc = await readRegistry();
76
+ return Object.hasOwn(doc.entries, id) ? doc.entries[id] : null;
77
+ }
78
+
79
+ /** Insert a new record; false when the id is already taken. */
80
+ export function insertRecord(record) {
81
+ return mutateRegistry((doc) => {
82
+ if (Object.hasOwn(doc.entries, record.id)) return false;
83
+ doc.entries[record.id] = { ...record, updatedAt: Date.now() };
84
+ return true;
85
+ });
86
+ }
87
+
88
+ export function putRecord(record) {
89
+ return mutateRegistry((doc) => {
90
+ doc.entries[record.id] = { ...record, updatedAt: Date.now() };
91
+ });
92
+ }
93
+
94
+ export function deleteRecord(id) {
95
+ return mutateRegistry((doc) => {
96
+ delete doc.entries[id];
97
+ });
98
+ }
99
+
100
+ /** Public shape: the contract entry plus the merge target and warnings. */
101
+ export function entryView(record) {
102
+ return {
103
+ ...normalizeWorktreeEntry(record),
104
+ // Repository-relative folder of a subfolder Project ('' at the root).
105
+ projectSubdir: typeof record.projectSubdir === 'string' ? record.projectSubdir : '',
106
+ mergeTarget: record.mergeTarget ?? null,
107
+ warnings: Array.isArray(record.warnings) ? [...record.warnings] : [],
108
+ disposable: record.disposable === true,
109
+ };
110
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Worktree settings, persisted at agent config `worktree`. Defaults live in
3
+ * code; the stored section holds only what the user changed.
4
+ */
5
+ import { isPlainObject } from '../object.mjs';
6
+ import { DEFAULT_WORKTREE_BRANCH_PREFIX } from './contract.mjs';
7
+
8
+ /** `branch`: new tasks use the project folder's current branch (worktree off). */
9
+ export const WORKTREE_NEW_TASK_DEFAULTS = Object.freeze(['branch', 'worktree']);
10
+
11
+ /**
12
+ * @typedef {object} WorktreeSettings
13
+ * @property {'branch'|'worktree'} newTaskDefault
14
+ * @property {string} branchPrefix Ref prefix for created branches; ends with "/".
15
+ * @property {number} sizeWarnBytes Repository size that asks for confirmation; 0 never asks.
16
+ * @property {boolean} autoCleanup Remove clean, merged worktrees when their session ends.
17
+ */
18
+
19
+ /** @type {Readonly<WorktreeSettings>} */
20
+ export const WORKTREE_SETTINGS_DEFAULTS = Object.freeze({
21
+ newTaskDefault: 'branch',
22
+ branchPrefix: DEFAULT_WORKTREE_BRANCH_PREFIX,
23
+ sizeWarnBytes: 2 * 1024 ** 3,
24
+ autoCleanup: true,
25
+ });
26
+
27
+ const KEYS = Object.keys(WORKTREE_SETTINGS_DEFAULTS);
28
+ const PREFIX_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*(\/[A-Za-z0-9][A-Za-z0-9._-]*)*\/$/;
29
+
30
+ /** Why `value` cannot be stored for `key`, or null. */
31
+ function fieldError(key, value) {
32
+ switch (key) {
33
+ case 'newTaskDefault':
34
+ return WORKTREE_NEW_TASK_DEFAULTS.includes(value)
35
+ ? null
36
+ : `must be one of ${WORKTREE_NEW_TASK_DEFAULTS.join(', ')}`;
37
+ case 'branchPrefix':
38
+ return typeof value === 'string' &&
39
+ value.length <= 64 &&
40
+ PREFIX_PATTERN.test(value) &&
41
+ !value.includes('..') &&
42
+ !/\.lock\//i.test(value)
43
+ ? null
44
+ : 'must be a ref path ending with "/", e.g. "mixdog/"';
45
+ case 'sizeWarnBytes':
46
+ return Number.isSafeInteger(value) && value >= 0 ? null : 'must be a non-negative integer';
47
+ case 'autoCleanup':
48
+ return typeof value === 'boolean' ? null : 'must be a boolean';
49
+ default:
50
+ return 'is not a worktree setting';
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Effective settings from the stored section. Invalid stored values fall back
56
+ * to their default so a hand edit never breaks boot.
57
+ * @param {unknown} stored
58
+ * @returns {WorktreeSettings}
59
+ */
60
+ export function normalizeWorktreeSettings(stored) {
61
+ const source = isPlainObject(stored) ? stored : {};
62
+ const settings = { ...WORKTREE_SETTINGS_DEFAULTS };
63
+ for (const key of KEYS) {
64
+ if (Object.hasOwn(source, key) && fieldError(key, source[key]) === null) settings[key] = source[key];
65
+ }
66
+ return settings;
67
+ }
68
+
69
+ /**
70
+ * Apply a user patch strictly: unknown keys and invalid values throw.
71
+ * @param {unknown} current Stored or effective settings.
72
+ * @param {Partial<WorktreeSettings>} patch
73
+ * @returns {WorktreeSettings}
74
+ */
75
+ export function applyWorktreeSettingsPatch(current, patch) {
76
+ if (!isPlainObject(patch) || !Object.keys(patch).length) {
77
+ throw new TypeError('worktree settings require at least one setting.');
78
+ }
79
+ for (const [key, value] of Object.entries(patch)) {
80
+ const error = fieldError(key, value);
81
+ if (error) throw new TypeError(`worktree.${key} ${error}.`);
82
+ }
83
+ return { ...normalizeWorktreeSettings(current), ...patch };
84
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Local worktree slug generator: a short, Windows-safe folder name and branch
3
+ * suffix derived from the task's first message, made unique on demand.
4
+ */
5
+ import { randomBytes } from 'node:crypto';
6
+ import { worktreeNameError } from './contract.mjs';
7
+
8
+ export const SLUG_MAX_LENGTH = 40;
9
+ const FALLBACK_SLUG = 'task';
10
+ // DOS device names stay reserved on Windows, even with an extension.
11
+ const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i;
12
+
13
+ function trimSlug(value, max) {
14
+ return value.slice(0, max).replace(/-+$/, '');
15
+ }
16
+
17
+ /**
18
+ * @param {unknown} hint Free text, typically the first user message.
19
+ * @returns {string} Lowercase [a-z0-9-], at most SLUG_MAX_LENGTH characters.
20
+ */
21
+ export function slugFromHint(hint) {
22
+ const words = String(hint ?? '')
23
+ .normalize('NFKD')
24
+ .replace(/[\u0300-\u036f]/g, '')
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, ' ')
27
+ .trim()
28
+ .split(/\s+/)
29
+ .filter(Boolean);
30
+ let slug = '';
31
+ for (const word of words) {
32
+ const next = slug ? `${slug}-${word}` : word;
33
+ if (next.length > SLUG_MAX_LENGTH) {
34
+ if (!slug) slug = trimSlug(word, SLUG_MAX_LENGTH);
35
+ break;
36
+ }
37
+ slug = next;
38
+ }
39
+ if (!slug) slug = FALLBACK_SLUG;
40
+ if (WINDOWS_RESERVED.test(slug)) slug = `${slug}-${FALLBACK_SLUG}`;
41
+ return slug;
42
+ }
43
+
44
+ /**
45
+ * First of base, base-2, base-3, … that `isTaken` rejects, then a random
46
+ * suffix. Every candidate passes worktreeNameError.
47
+ * @param {string} base
48
+ * @param {(slug: string) => boolean} isTaken
49
+ */
50
+ export function uniqueSlug(base, isTaken) {
51
+ const root = worktreeNameError(base) ? FALLBACK_SLUG : base;
52
+ for (let attempt = 1; attempt <= 50; attempt++) {
53
+ const suffix = attempt === 1 ? '' : `-${attempt}`;
54
+ const candidate = `${trimSlug(root, SLUG_MAX_LENGTH - suffix.length)}${suffix}`;
55
+ if (!worktreeNameError(candidate) && !isTaken(candidate)) return candidate;
56
+ }
57
+ for (;;) {
58
+ const suffix = `-${randomBytes(4).toString('hex')}`;
59
+ const candidate = `${trimSlug(root, SLUG_MAX_LENGTH - suffix.length)}${suffix}`;
60
+ if (!isTaken(candidate)) return candidate;
61
+ }
62
+ }
@@ -0,0 +1,96 @@
1
+ // Temporary git repositories and data dirs for the worktree suites.
2
+ import { execFileSync } from 'node:child_process';
3
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ const created = [];
8
+
9
+ export function tempDir(prefix = 'mixdog-wt-') {
10
+ const dir = mkdtempSync(join(tmpdir(), prefix));
11
+ created.push(dir);
12
+ return dir;
13
+ }
14
+
15
+ export function cleanupTempDirs() {
16
+ for (const dir of created.splice(0)) {
17
+ try {
18
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
19
+ } catch {
20
+ /* best effort */
21
+ }
22
+ }
23
+ }
24
+
25
+ const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_EDITOR: 'true' };
26
+ for (const name of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE']) delete GIT_ENV[name];
27
+
28
+ export function git(cwd, ...args) {
29
+ return execFileSync('git', args, {
30
+ cwd,
31
+ env: GIT_ENV,
32
+ encoding: 'utf8',
33
+ windowsHide: true,
34
+ stdio: ['ignore', 'pipe', 'pipe'],
35
+ }).trim();
36
+ }
37
+
38
+ export function writeFile(root, rel, content) {
39
+ const path = join(root, ...rel.split('/'));
40
+ mkdirSync(dirname(path), { recursive: true });
41
+ writeFileSync(path, content);
42
+ return path;
43
+ }
44
+
45
+ /** A git hook script; outside Windows git skips a hook without the executable bit. */
46
+ export function writeHook(root, name, script) {
47
+ const path = writeFile(root, `.git/hooks/${name}`, script);
48
+ chmodSync(path, 0o755);
49
+ return path;
50
+ }
51
+
52
+ export function commitFile(cwd, rel, content, message = `edit ${rel}`) {
53
+ writeFile(cwd, rel, content);
54
+ git(cwd, 'add', '--', rel);
55
+ git(cwd, 'commit', '-q', '-m', message);
56
+ return git(cwd, 'rev-parse', 'HEAD');
57
+ }
58
+
59
+ /** A repository on `main` with one commit. */
60
+ export function makeRepo({ commit = true } = {}) {
61
+ const dir = tempDir('mixdog-wt-repo-');
62
+ git(dir, 'init', '-q', '-b', 'main');
63
+ git(dir, 'config', 'user.name', 'Mixdog Test');
64
+ git(dir, 'config', 'user.email', 'mixdog@example.com');
65
+ git(dir, 'config', 'core.autocrlf', 'false');
66
+ if (commit) commitFile(dir, 'README.md', 'hello\n', 'initial');
67
+ return dir;
68
+ }
69
+
70
+ /** Point the data dir at a fresh temp dir; returns it. */
71
+ export function useTempDataDir() {
72
+ const dir = tempDir('mixdog-wt-data-');
73
+ process.env.MIXDOG_DATA_DIR = dir;
74
+ return dir;
75
+ }
76
+
77
+ let idCounter = 0;
78
+
79
+ /** Fresh session/runtime/transaction ids for one managed worktree. */
80
+ export function nextIds() {
81
+ idCounter += 1;
82
+ return { sessionId: `session-${idCounter}`, runtimeToken: `token-${idCounter}`, txId: `tx-${idCounter}` };
83
+ }
84
+
85
+ /** createManagedWorktree with fresh ids; resolves { entry, sessionId, runtimeToken, txId, ...extra }. */
86
+ export async function createTestWorktree(projectPath, extra = {}) {
87
+ const { createManagedWorktree } = await import('./manager.mjs');
88
+ const id = nextIds();
89
+ const entry = await createManagedWorktree({ projectPath, nameHint: 'Fix the login page', ...id, ...extra });
90
+ return { entry, ...id, ...extra };
91
+ }
92
+
93
+ /** The repository's branches under the default managed prefix. */
94
+ export function prefixedBranches(repo) {
95
+ return git(repo, 'for-each-ref', '--format=%(refname:short)', 'refs/heads/mixdog/').split('\n').filter(Boolean);
96
+ }