mixdog 1.4.1 → 1.4.2

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 (326) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/gate-local.mjs +172 -61
  4. package/scripts/lib/gate-scheduler.mjs +138 -32
  5. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  6. package/scripts/lib/run-node-tests.mjs +54 -29
  7. package/scripts/lib/scoped-temp.mjs +1 -0
  8. package/scripts/lib/test-failure-records.mjs +14 -1
  9. package/scripts/lib/test-failure-summary.mjs +16 -4
  10. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  11. package/scripts/pptx-kit-index.mjs +5 -6
  12. package/scripts/suite-health.mjs +110 -28
  13. package/scripts/test.mjs +9 -14
  14. package/src/agents/advisor/AGENT.md +4 -4
  15. package/src/agents/advisor/agent.json +1 -1
  16. package/src/agents/explore/AGENT.md +22 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/cli.mjs +7 -0
  19. package/src/defaults/agents.json +7 -0
  20. package/src/defaults/shipped-history.json +18 -2
  21. package/src/defaults/skills/browser-use/SKILL.md +4 -1
  22. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  23. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  24. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  25. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  26. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  27. package/src/defaults/skills/computer-use/SKILL.md +12 -2
  28. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  29. package/src/defaults/skills/setup/references/actions.md +12 -6
  30. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  31. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  32. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  33. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  34. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  35. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  36. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  37. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  38. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  39. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  40. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  41. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  42. package/src/defaults/skills/visualize/SKILL.md +6 -2
  43. package/src/lib/rules-builder.cjs +6 -30
  44. package/src/rules/lead/LEAD.md +3 -3
  45. package/src/rules/shared/20-research.md +3 -6
  46. package/src/rules/shared/40-editing.md +1 -0
  47. package/src/rules/shared/60-verification.md +10 -9
  48. package/src/rules/shared/70-delivery.md +4 -3
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
  52. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  54. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  55. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
  56. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  57. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  58. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  59. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  60. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  61. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  63. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
  64. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  65. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
  66. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
  67. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  69. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  70. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  74. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  75. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  78. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  80. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
  81. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  82. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  83. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  85. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
  88. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
  93. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  94. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  95. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  96. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  97. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  98. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
  99. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  100. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  102. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  103. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  104. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  105. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
  106. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
  107. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
  108. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
  109. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
  110. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  111. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  112. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  116. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  117. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  118. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  123. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
  124. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
  125. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  126. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  127. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
  128. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
  129. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  130. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  131. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
  132. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
  133. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  134. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  135. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  136. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  137. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
  138. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
  139. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  143. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  144. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  145. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  154. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  155. package/src/runtime/channels/lib/whisper-port.mjs +19 -10
  156. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  157. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  158. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  159. package/src/runtime/explore/explorer.mjs +93 -0
  160. package/src/runtime/explore/tool-defs.mjs +35 -0
  161. package/src/runtime/explore/tool.mjs +123 -0
  162. package/src/runtime/memory/index.mjs +4 -0
  163. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  164. package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
  165. package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
  166. package/src/runtime/shared/agent-route-config.mjs +17 -2
  167. package/src/runtime/shared/atomic-file.mjs +103 -2
  168. package/src/runtime/shared/background-tasks.mjs +15 -6
  169. package/src/runtime/shared/connector-accounts.mjs +5 -5
  170. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  171. package/src/runtime/shared/connector-catalog.mjs +60 -52
  172. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  173. package/src/runtime/shared/file-has-data.mjs +26 -2
  174. package/src/runtime/shared/file-lock.mjs +13 -15
  175. package/src/runtime/shared/git-launch.d.mts +9 -0
  176. package/src/runtime/shared/git-launch.mjs +213 -0
  177. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  178. package/src/runtime/shared/llm/cost.mjs +6 -1
  179. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  180. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  181. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  182. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  183. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  184. package/src/runtime/shared/provider-accounts.mjs +52 -2
  185. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  186. package/src/runtime/shared/staged-update.mjs +47 -2
  187. package/src/runtime/shared/tool-surface.mjs +110 -36
  188. package/src/runtime/shared/tool-work-units.mjs +4 -0
  189. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  190. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  191. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  192. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  193. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  194. package/src/runtime/shared/worktrees/git.mjs +57 -0
  195. package/src/runtime/shared/worktrees/manager.mjs +205 -137
  196. package/src/runtime/tidy/install.mjs +95 -15
  197. package/src/runtime/tidy/languages.mjs +17 -15
  198. package/src/runtime/tidy/process.mjs +7 -4
  199. package/src/runtime/tidy/resolve.mjs +19 -16
  200. package/src/runtime/tidy/runners/biome.mjs +3 -3
  201. package/src/runtime/tidy/tool.mjs +1 -1
  202. package/src/runtime/tidy/worktree.mjs +10 -6
  203. package/src/runtime/visualize-bridge/client.mjs +4 -1
  204. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  205. package/src/runtime/web-search/index.mjs +1 -1
  206. package/src/session-runtime/agent-task-sessions.mjs +410 -353
  207. package/src/session-runtime/boot/tools.mjs +2 -0
  208. package/src/session-runtime/bot-status.mjs +103 -103
  209. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  210. package/src/session-runtime/conversation-import/discovery.mjs +159 -40
  211. package/src/session-runtime/conversation-import/service.mjs +9 -3
  212. package/src/session-runtime/handoff-attachments.mjs +1 -1
  213. package/src/session-runtime/held-requests.mjs +3 -1
  214. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  215. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  216. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
  217. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  218. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  219. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  220. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  221. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  222. package/src/session-runtime/provider-auth/login.mjs +2 -2
  223. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  224. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  225. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  226. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  227. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  228. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  229. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  230. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  231. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  232. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  233. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  234. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  235. package/src/session-runtime/services/provider-admin.mjs +281 -10
  236. package/src/session-runtime/session-turn-api.mjs +2 -0
  237. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  238. package/src/session-runtime/settings-system-api.mjs +1 -1
  239. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  240. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  241. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  242. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  243. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  244. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  245. package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
  246. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  247. package/src/session-runtime/workspace/session-view.mjs +149 -0
  248. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  249. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  250. package/src/session-runtime/workspace/transitions.mjs +108 -1066
  251. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  252. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  253. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  254. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  255. package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
  256. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  257. package/src/standalone/daemon-session-control.mjs +151 -136
  258. package/src/standalone/daemon.mjs +7 -0
  259. package/src/standalone/desktop-service-registry.mjs +9 -0
  260. package/src/standalone/local-session-runtime.mjs +5 -1
  261. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  262. package/src/standalone/session-client.mjs +5 -1
  263. package/src/standalone/session-frame-stream.mjs +83 -11
  264. package/src/standalone/session-protocol.mjs +4 -0
  265. package/src/standalone/session-service/agent-tree.mjs +45 -9
  266. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  267. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  268. package/src/standalone/session-service/stored-reader.mjs +127 -40
  269. package/src/tui/App.jsx +154 -164
  270. package/src/tui/app/app-view/floating-panel.jsx +3 -0
  271. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  272. package/src/tui/app/clipboard.mjs +21 -10
  273. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  274. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  275. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  276. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  277. package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
  278. package/src/tui/app/provider-setup-picker.mjs +20 -13
  279. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  280. package/src/tui/app/slash-commands.mjs +1 -0
  281. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  282. package/src/tui/app/slash-dispatch.mjs +1 -1
  283. package/src/tui/app/use-btw-panel.mjs +29 -0
  284. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  285. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  286. package/src/tui/app/use-mouse-input.mjs +11 -0
  287. package/src/tui/app/use-onboarding-start.mjs +8 -3
  288. package/src/tui/app/use-panel-transition.mjs +10 -1
  289. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  290. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  291. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  292. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  293. package/src/tui/app/use-selection-focus.mjs +11 -2
  294. package/src/tui/app/use-selection-geometry.mjs +18 -9
  295. package/src/tui/app/use-selection-paint.mjs +4 -4
  296. package/src/tui/app/use-selection-stitch.mjs +3 -3
  297. package/src/tui/app/use-slash-palette.mjs +62 -0
  298. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  299. package/src/tui/app/use-transcript-activity.mjs +47 -32
  300. package/src/tui/app/use-transcript-follow.mjs +12 -4
  301. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  302. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  303. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  304. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  305. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  306. package/src/tui/app/use-transcript-window.mjs +21 -11
  307. package/src/tui/app/use-tui-ready.mjs +18 -0
  308. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  309. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  310. package/src/tui/components/Picker.jsx +27 -3
  311. package/src/tui/components/SignInUrl.jsx +68 -0
  312. package/src/tui/components/TextEntryPanel.jsx +76 -46
  313. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  314. package/src/tui/dist/index.mjs +1896 -1341
  315. package/src/tui/session/goal-turn-state.mjs +6 -2
  316. package/src/tui/session/session-api/controls.mjs +4 -0
  317. package/src/tui/session/session-api/extensions.mjs +8 -0
  318. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  319. package/src/tui/session/session-api/intake.mjs +2 -0
  320. package/src/tui/session/session-api/integrations.mjs +2 -2
  321. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  322. package/src/tui/session/session-api/settings.mjs +2 -2
  323. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  324. package/src/tui/session/transcript-spill.mjs +92 -4
  325. package/src/tui/session/transcript-store.mjs +2 -0
  326. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -72,6 +72,27 @@ export function createAgentOps(deps) {
72
72
  });
73
73
  }
74
74
 
75
+ // `/stop`: the background shells this session would stop, then stopping all
76
+ // of them through the same per-task cancel.
77
+ async function listStoppableShellJobs() {
78
+ const session = getSession();
79
+ const { listStoppableShellJobs: list } = await import(
80
+ '../../runtime/agent/orchestrator/tools/builtin/task-tool.mjs'
81
+ );
82
+ return {
83
+ jobs: list({ callerSessionId: session?.id || null, clientHostPid: session?.clientHostPid || process.pid }),
84
+ };
85
+ }
86
+
87
+ async function stopShellJobs() {
88
+ const session = getSession();
89
+ const { stopBackgroundShellJobs } = await import('../../runtime/agent/orchestrator/tools/builtin/task-tool.mjs');
90
+ return stopBackgroundShellJobs({
91
+ callerSessionId: session?.id || null,
92
+ clientHostPid: session?.clientHostPid || process.pid,
93
+ });
94
+ }
95
+
75
96
  // Explicit user force-release ("Release anyway") of a shell job whose
76
97
  // termination is unconfirmed: drops its workspace hold. The surface has
77
98
  // already asked the user to confirm. The caller session's own holds, and
@@ -147,6 +168,8 @@ export function createAgentOps(deps) {
147
168
  agentControl,
148
169
  taskControl,
149
170
  stopShellJob,
171
+ listStoppableShellJobs,
172
+ stopShellJobs,
150
173
  releaseShellJob,
151
174
  listShellJobs,
152
175
  onNotification,
@@ -4,6 +4,9 @@ import {
4
4
  assertConnectorPolicyNotWidened,
5
5
  CONNECTOR_PERMISSIONS,
6
6
  connectorEnabled,
7
+ connectorGroupId,
8
+ connectorToolEntries,
9
+ connectorToolPermission,
7
10
  DENY_ALL_CONNECTORS,
8
11
  migrateLegacyConnectorPolicy,
9
12
  NO_WIDEN_MESSAGE,
@@ -108,8 +111,8 @@ export function createAgentConnectorsApi(deps, editor) {
108
111
  return getAgentConnectors(agentId);
109
112
  }
110
113
 
111
- // Older per-tool overrides keep working (the profile no longer shows them):
112
- // `permission` null drops the tool's entry, so the tool is auto again.
114
+ // One tool's permission (the profile's tool list, getAgentConnectorTools):
115
+ // `permission` null drops the tool's entry, so the tool is on its default again.
113
116
  async function setAgentConnectorToolPermission(agentId, connectorId, toolName, permission, options) {
114
117
  if (permission != null && !CONNECTOR_PERMISSIONS.includes(permission)) {
115
118
  throw new Error(`permission must be one of ${CONNECTOR_PERMISSIONS.join(', ')}`);
@@ -117,19 +120,25 @@ export function createAgentConnectorsApi(deps, editor) {
117
120
  const server = requireId(connectorId, 'connector id');
118
121
  const tool = requireId(toolName, 'tool name');
119
122
  const policy = editablePolicy(agentId);
120
- const own = { ...policy.tools[server] };
123
+ // The accounts of one connector (`gmail`, `gmail-2`…) share the permission,
124
+ // stored once under the connector's id; an entry an account kept of its own
125
+ // for this tool goes in the same write.
126
+ const group = connectorGroupId(server);
127
+ const tools = Object.fromEntries(
128
+ Object.entries(policy.tools).map(([id, entries]) => {
129
+ if (id === group || connectorGroupId(id) !== group) return [id, entries];
130
+ const { [tool]: _dropped, ...rest } = entries;
131
+ return [id, rest];
132
+ })
133
+ );
134
+ const own = { ...tools[group] };
121
135
  if (permission == null) delete own[tool];
122
136
  else own[tool] = permission;
137
+ tools[group] = own;
123
138
  if (options?.noWiden) {
124
- assertConnectorPolicyNotWidened(editor.getAgentConnectorPolicy(agentId), {
125
- off: policy.off,
126
- tools: { ...policy.tools, [server]: own },
127
- });
139
+ assertConnectorPolicyNotWidened(editor.getAgentConnectorPolicy(agentId), { off: policy.off, tools });
128
140
  }
129
- return editor.setAgentConnectorPolicy(
130
- agentId,
131
- storedConnectorPolicy({ off: policy.off, tools: { ...policy.tools, [server]: own } })
132
- );
141
+ return editor.setAgentConnectorPolicy(agentId, storedConnectorPolicy({ off: policy.off, tools }));
133
142
  }
134
143
 
135
144
  // A card's "항상 허용" for a connector tool saves through this same writer.
@@ -149,6 +158,34 @@ export function createAgentConnectorsApi(deps, editor) {
149
158
  .sort((a, b) => a.connector.localeCompare(b.connector) || a.tool.localeCompare(b.tool));
150
159
  }
151
160
 
161
+ /** `[{ connector, tool, permission, explicit }]`: each installed connector's
162
+ * tools (the connected server's own, and any the agent's policy names) with
163
+ * the permission the agent has for the tool itself, so a connector switched
164
+ * off does not change it, and whether that is an entry of its own policy
165
+ * rather than the tool's default. The profile's per-tool editor reads it. */
166
+ function getAgentConnectorTools(agentId) {
167
+ const { tools } = editablePolicy(agentId);
168
+ return installedIds().flatMap((connector) => {
169
+ const group = connectorGroupId(connector);
170
+ const accounts = Object.entries(tools).filter(([id]) => connectorGroupId(id) === group);
171
+ const names = new Set([
172
+ ...listConnectorTools(connector).map(({ name }) => name),
173
+ ...accounts.flatMap(([, entries]) => Object.keys(entries)),
174
+ ]);
175
+ // Accounts share one permission (stored under the connector); entries an
176
+ // account kept of its own follow the topmost.
177
+ return [...names].map((tool) => ({
178
+ connector,
179
+ tool,
180
+ permission: connectorToolPermission(
181
+ { off: [], tools },
182
+ { mcpServerName: connector, mcpToolName: tool }
183
+ ),
184
+ explicit: connectorToolEntries(tools, group, tool).length > 0,
185
+ }));
186
+ });
187
+ }
188
+
152
189
  /** Replace the agent's connector policy (null removes it). With `options.noWiden`
153
190
  * the replacement may not give any tool more authority than the stored policy. */
154
191
  async function setAgentConnectors(agentId, connectors, options) {
@@ -279,6 +316,7 @@ export function createAgentConnectorsApi(deps, editor) {
279
316
  setAgentConnectorEnabled,
280
317
  setAgentConnectorToolPermission,
281
318
  getAgentAlwaysAllowedTools,
319
+ getAgentConnectorTools,
282
320
  getProjectConnectors,
283
321
  setProjectConnectorEnabled,
284
322
  migrateConnectorPolicies,
@@ -7,6 +7,7 @@ import { ensureProviderEnabled } from '../../runtime/agent/orchestrator/runtime-
7
7
  import { resolveRouteContextState, selectionFastCapable } from '../route-state.mjs';
8
8
  import {
9
9
  canonicalizeAgentRouteStorage,
10
+ EXPLORE_AGENT_ID,
10
11
  isAgentDisabled,
11
12
  withAgentDisabled,
12
13
  } from '../../runtime/shared/agent-route-config.mjs';
@@ -226,5 +227,13 @@ export function createAgentRouteApi(deps) {
226
227
  return stayOff ? { ...routeToSave, id, disabled: true } : routeToSave;
227
228
  }
228
229
 
229
- return { setAgentRoute };
230
+ // Explore's route decides whether the explore tool exists, so the empty
231
+ // session rebuilds its tool surface on any change of it.
232
+ async function setAgentRouteAndRefresh(agentId, next) {
233
+ const saved = await setAgentRoute(agentId, next);
234
+ if (agentEditorId(agentId) === EXPLORE_AGENT_ID) await deps.refreshEmptySessionToolPolicy?.();
235
+ return saved;
236
+ }
237
+
238
+ return { setAgentRoute: setAgentRouteAndRefresh };
230
239
  }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * The activity checks every workspace change runs first: whether the hosted
3
+ * Lead (its turn, workers and background shells) is idle, and whether other
4
+ * sessions work in a shared checkout — then holding that checkout so new
5
+ * turns there wait until the change settles (admission gate).
6
+ */
7
+ import { worktreeError, worktreeRefusal } from '../../runtime/shared/worktrees/contract.mjs';
8
+ import { isWithin } from '../../runtime/shared/worktrees/paths.mjs';
9
+ import {
10
+ holdWorkspaceActivity,
11
+ isExternalCheckout,
12
+ releaseAll,
13
+ } from '../../runtime/shared/worktrees/activity-gate.mjs';
14
+ import { AGENT_OWNER } from '../../runtime/agent/orchestrator/agent-owner.mjs';
15
+ import { holdCheckoutFolder } from './admission-gate.mjs';
16
+
17
+ const ACTIVE_STAGES = new Set(['connecting', 'requesting', 'streaming', 'tool_running', 'running', 'cancelling']);
18
+ const BUSY_WORKER_STATES = new Set([...ACTIVE_STAGES, 'starting', 'spawning', 'queued', 'pending']);
19
+ const BUSY_JOB_STATES = new Set(['starting', 'spawning', 'queued', 'pending', 'running']);
20
+
21
+ const lower = (value) => String(value || '').toLowerCase();
22
+
23
+ /** @param {object} deps The activity sources of createWorkspaceTransitions. */
24
+ export function createActivityGuards({
25
+ getActiveTurnCount,
26
+ agentStatusState,
27
+ countOwnShellJobs,
28
+ listLiveSessions,
29
+ sessionManager,
30
+ listWorkspaceConsumers,
31
+ listWorkspaceJobs,
32
+ }) {
33
+ function isSessionActive(session) {
34
+ const runtime = sessionManager?.getSessionRuntime?.(session.id);
35
+ if (runtime?.controller?.signal && !runtime.controller.signal.aborted) return true;
36
+ return ACTIVE_STAGES.has(lower(runtime?.stage || session.status));
37
+ }
38
+
39
+ const workersOf = (session) =>
40
+ listLiveSessions().filter(
41
+ (candidate) =>
42
+ candidate?.id &&
43
+ candidate.id !== session.id &&
44
+ candidate.owner === AGENT_OWNER &&
45
+ candidate.closed !== true &&
46
+ (candidate.ownerSessionId === session.id || candidate.parentSessionId === session.id)
47
+ );
48
+
49
+ /**
50
+ * WORKTREE_BUSY_JOBS while this Lead has a turn, workers or background
51
+ * shells running (`shellJobs: false` tolerates background shells).
52
+ */
53
+ async function assertLeadIdle(session, { ownTurn = false, shellJobs: checkShellJobs = true } = {}) {
54
+ if (!ownTurn && getActiveTurnCount() > 0) {
55
+ throw worktreeError('WORKTREE_BUSY_JOBS', 'The session is running a turn; wait for it to finish.', {
56
+ reason: 'turn',
57
+ });
58
+ }
59
+ const status = agentStatusState?.() || {};
60
+ const workers = Array.isArray(status.agentWorkers) ? status.agentWorkers : [];
61
+ const jobs = Array.isArray(status.agentJobs) ? status.agentJobs : [];
62
+ const busyWorkers = workers.filter(
63
+ (row) => BUSY_WORKER_STATES.has(lower(row?.status)) || BUSY_WORKER_STATES.has(lower(row?.stage))
64
+ ).length;
65
+ const busyJobs = jobs.filter((row) => BUSY_JOB_STATES.has(lower(row?.status))).length;
66
+ if (busyWorkers || busyJobs) {
67
+ throw worktreeError('WORKTREE_BUSY_JOBS', undefined, { reason: 'workers', workers: busyWorkers, jobs: busyJobs });
68
+ }
69
+ if (!checkShellJobs) return;
70
+ const ids = new Set([session.id, ...workers.map((row) => row?.sessionId).filter(Boolean)]);
71
+ for (const worker of workersOf(session)) ids.add(worker.id);
72
+ const shellJobs = await countOwnShellJobs([...ids]);
73
+ if (shellJobs > 0) throw worktreeError('WORKTREE_BUSY_JOBS', undefined, { reason: 'shell-jobs', shellJobs });
74
+ }
75
+
76
+ /**
77
+ * A checkout or merge in the shared checkout `folder` refuses while another
78
+ * session (not this Lead's own workers) works there, then holds the folder
79
+ * until the returned release: other sessions' turns that arrive meanwhile
80
+ * wait (admission gate). `sessionId` null: a session-less (draft) change.
81
+ * @returns {() => void}
82
+ */
83
+ function holdProjectFolder(sessionId, folder, { hold = true } = {}) {
84
+ const users = listLiveSessions().filter(
85
+ (candidate) =>
86
+ candidate?.id &&
87
+ candidate.id !== sessionId &&
88
+ candidate.closed !== true &&
89
+ (!sessionId || (candidate.ownerSessionId !== sessionId && candidate.parentSessionId !== sessionId)) &&
90
+ typeof candidate.cwd === 'string' &&
91
+ isWithin(folder, candidate.cwd) &&
92
+ isSessionActive(candidate)
93
+ );
94
+ if (users.length) throw worktreeRefusal('folder-in-use', { sessions: users.map((candidate) => candidate.id) });
95
+ return holdCheckoutFolder(sessionId, folder, { hold });
96
+ }
97
+
98
+ /** Run `fn` holding the checkout `folder` (holdProjectFolder); released when it settles. */
99
+ async function withProjectFolder(sessionId, folder, fn) {
100
+ const release = holdProjectFolder(sessionId, folder);
101
+ try {
102
+ return await fn();
103
+ } finally {
104
+ release();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Before a carry clears the shared checkout `folder` (held by the caller,
110
+ * so no new turn starts there): WORKTREE_BUSY_JOBS while any other session
111
+ * working in it — idle or not, this Lead's workers included — owns a
112
+ * running background shell (holdProjectFolder already refused running
113
+ * turns and agents there, assertLeadIdle this Lead's own). Processes
114
+ * outside Mixdog (editors, watchers) cannot be excluded.
115
+ */
116
+ async function assertCheckoutQuiet(session, folder) {
117
+ const sharing = listLiveSessions()
118
+ .filter(
119
+ (candidate) =>
120
+ candidate?.id &&
121
+ candidate.id !== session.id &&
122
+ candidate.closed !== true &&
123
+ typeof candidate.cwd === 'string' &&
124
+ isWithin(folder, candidate.cwd)
125
+ )
126
+ .map((candidate) => candidate.id);
127
+ if (!sharing.length) return;
128
+ const shellJobs = await countOwnShellJobs(sharing);
129
+ if (shellJobs > 0) throw worktreeRefusal('folder-shell-jobs', { shellJobs });
130
+ }
131
+
132
+ /**
133
+ * The shared admission of every merge and folder cleanup (manageProjectBranch):
134
+ * refuses while a session or worker runs in an affected folder, or a
135
+ * background command does; `removing` refuses while any session still uses
136
+ * one. Then holds the folders — Mixdog checkouts only; an external checkout
137
+ * takes no lease from this profile — so new Mixdog work there waits until `run`
138
+ * settles. `actor`: the session acting on its own checkout, whose own turn
139
+ * and workers its caller already checked (assertLeadIdle).
140
+ */
141
+ async function withManagementFolders(folders, removing, run, { actor = null } = {}) {
142
+ const release = [];
143
+ try {
144
+ for (const folder of [...new Set(folders)].sort()) {
145
+ release.push(holdProjectFolder(actor, folder, { hold: !isExternalCheckout(folder) }));
146
+ }
147
+ release.push(holdWorkspaceActivity(folders, { sessionId: actor }));
148
+ const stored = await listWorkspaceConsumers();
149
+ const consumers = new Map(stored.map((session) => [session.id, session]));
150
+ for (const session of listLiveSessions()) if (session?.closed !== true) consumers.set(session.id, session);
151
+ const affected = [...consumers.values()].filter((session) =>
152
+ folders.some((folder) => session.cwd && isWithin(folder, session.cwd))
153
+ );
154
+ const blocking = removing
155
+ ? affected
156
+ : affected.filter(
157
+ (session) =>
158
+ !(actor && [session.id, session.ownerSessionId, session.parentSessionId].includes(actor)) &&
159
+ isSessionActive(session)
160
+ );
161
+ if (blocking.length) throw worktreeRefusal('folder-in-use', { sessions: blocking.map((session) => session.id) });
162
+ const jobs = await listWorkspaceJobs();
163
+ if (jobs.some((job) => !job.terminal && folders.some((folder) => isWithin(folder, job.cwd)))) {
164
+ throw worktreeRefusal('folder-shell-jobs');
165
+ }
166
+ if ((await countOwnShellJobs(affected.map((session) => session.id))) > 0) {
167
+ throw worktreeRefusal('folder-shell-jobs');
168
+ }
169
+ return await run([...consumers.keys()]);
170
+ } finally {
171
+ releaseAll(release);
172
+ }
173
+ }
174
+
175
+ return { workersOf, assertLeadIdle, withProjectFolder, assertCheckoutQuiet, withManagementFolders };
176
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The hosted Lead's workspace as the transitions read it: the session this
3
+ * runtime hosts, its stored (or project-folder) workspace record, the live
4
+ * view the session snapshot publishes and the branch menu.
5
+ */
6
+ import { isDeepStrictEqual } from 'node:util';
7
+ import {
8
+ branchMenuEntries,
9
+ isWorktreeBound,
10
+ normalizeSessionWorkspace,
11
+ worktreeError,
12
+ } from '../../runtime/shared/worktrees/contract.mjs';
13
+ import { samePath } from '../../runtime/shared/worktrees/paths.mjs';
14
+ import { readGitHead } from '../../runtime/shared/worktrees/git-head.mjs';
15
+ import { readSessionWorkspace } from './workspace-state.mjs';
16
+
17
+ /**
18
+ * @param {object} deps The host hooks of createWorkspaceTransitions plus its
19
+ * `backend()` loader and `pendingWorktree()` (the armed worktree intent).
20
+ */
21
+ export function createSessionWorkspaceView({
22
+ getSession,
23
+ getCurrentCwd,
24
+ getDesktopSession,
25
+ createCurrentSession,
26
+ inspectProject,
27
+ backend,
28
+ pendingWorktree,
29
+ }) {
30
+ function hostedSession(sessionRef) {
31
+ const session = getSession();
32
+ const requested = String(sessionRef?.sessionId || '').trim();
33
+ if (!session?.id || (requested && requested !== session.id)) {
34
+ throw new Error(`workspace: session ${requested || '(none)'} is not hosted by this runtime.`);
35
+ }
36
+ return session;
37
+ }
38
+
39
+ async function ensureSession() {
40
+ if (!getSession()?.id && typeof createCurrentSession === 'function') await createCurrentSession('workspace');
41
+ return hostedSession(null);
42
+ }
43
+
44
+ function projectPathOf(session) {
45
+ const desktop = getDesktopSession?.() || session?.desktopSession;
46
+ if (desktop?.classification === 'project' && desktop.projectPath) return desktop.projectPath;
47
+ return session?.cwd || getCurrentCwd();
48
+ }
49
+
50
+ /** The stored record, or the project-folder workspace (revision 0) of a session that never moved. */
51
+ function currentWorkspace(session) {
52
+ const stored = readSessionWorkspace(session);
53
+ if (stored) return stored;
54
+ const projectPath = projectPathOf(session);
55
+ return normalizeSessionWorkspace({ projectPath, cwd: getCurrentCwd() || projectPath, revision: 0 });
56
+ }
57
+
58
+ let liveWorkspace = null;
59
+
60
+ /**
61
+ * The workspace the session publishes: a worktree-bound record as stored; a
62
+ * project-folder session (never moved, back from a worktree, or carrying a
63
+ * transition stub) with the folder's checked-out branch / detached commit and
64
+ * repoCommonDir read straight from HEAD — no git process. Null outside a git
65
+ * repository for a session without a record. Identity-stable while equal.
66
+ */
67
+ function getLiveWorkspace() {
68
+ const session = getSession();
69
+ const stored = readSessionWorkspace(session);
70
+ if (isWorktreeBound(stored)) return session.workspace;
71
+ let base;
72
+ try {
73
+ base = stored ?? currentWorkspace(session);
74
+ } catch {
75
+ return session?.workspace ?? null;
76
+ }
77
+ const head = readGitHead(base.cwd);
78
+ if (!head) return session?.workspace ?? null;
79
+ const next = {
80
+ ...base,
81
+ repoCommonDir: head.repoCommonDir,
82
+ branch: head.branch,
83
+ baseCommit: head.branch ? base.baseCommit : head.commit,
84
+ };
85
+ if (!liveWorkspace || !isDeepStrictEqual(liveWorkspace, next)) liveWorkspace = next;
86
+ return liveWorkspace;
87
+ }
88
+
89
+ /** WORKTREE_PROJECT_MISMATCH unless `repoCommonDir` (when given) is the session Project's repository. */
90
+ async function sessionProject(session, repoCommonDir) {
91
+ const workspace = currentWorkspace(session);
92
+ const project = await inspectProject(workspace.projectPath);
93
+ if (repoCommonDir && !samePath(repoCommonDir, project.repoCommonDir)) {
94
+ throw worktreeError(
95
+ 'WORKTREE_PROJECT_MISMATCH',
96
+ "Managed worktrees belong to this session's Project repository; run the command there.",
97
+ { projectPath: workspace.projectPath, repoCommonDir }
98
+ );
99
+ }
100
+ return { workspace, project };
101
+ }
102
+
103
+ async function getBranchMenu() {
104
+ const session = getSession();
105
+ const workspace = session?.id
106
+ ? currentWorkspace(session)
107
+ : normalizeSessionWorkspace({ projectPath: projectPathOf(null), cwd: getCurrentCwd(), revision: 0 });
108
+ const menu = await (await backend()).listBranchOccupancy({
109
+ projectPath: workspace.projectPath,
110
+ sessionId: session?.id || null,
111
+ });
112
+ const selectedEntry = workspace.worktreeId ? await (await backend()).findWorktreeContaining(workspace.cwd) : null;
113
+ // A session outside a managed worktree (the project folder — never moved,
114
+ // or back from a worktree — or a linked checkout of the selected branch)
115
+ // is on whatever that folder has checked out now.
116
+ // Its repository is the project's (a never-moved session has no stored
117
+ // record), and a detached checkout shows branch null with the commit in
118
+ // baseCommit — never mistaken for "not a repository".
119
+ let view = workspace;
120
+ if (!isWorktreeBound(workspace)) {
121
+ const project = await inspectProject(workspace.cwd);
122
+ const checkedOut = project.currentBranch ?? null;
123
+ view = {
124
+ ...workspace,
125
+ repoCommonDir: project.repoCommonDir ?? workspace.repoCommonDir,
126
+ branch: checkedOut,
127
+ baseCommit: checkedOut ? workspace.baseCommit : (project.headCommit ?? null),
128
+ };
129
+ }
130
+ return {
131
+ workspace: view,
132
+ pendingWorktree: pendingWorktree(),
133
+ branches: branchMenuEntries(menu.branches, menu.occupancy),
134
+ occupancy: menu.occupancy,
135
+ retainedWorktree: selectedEntry ?? menu.retainedWorktree ?? null,
136
+ // Creation notes of the retained worktree (submodules, include entries).
137
+ warnings: Array.isArray(menu.retainedWorktree?.warnings) ? [...menu.retainedWorktree.warnings] : [],
138
+ };
139
+ }
140
+
141
+ return {
142
+ hostedSession,
143
+ ensureSession,
144
+ currentWorkspace,
145
+ getLiveWorkspace,
146
+ sessionProject,
147
+ getBranchMenu,
148
+ };
149
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Managed checkouts the hosted Lead makes without moving into them: a branch
3
+ * worktree (`git worktree add`) and the read-only snapshots of its turns,
4
+ * which the turn's admission release removes once nothing runs in them.
5
+ */
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { WORKTREE_SNAPSHOT_KIND, worktreeError } from '../../runtime/shared/worktrees/contract.mjs';
8
+ import { admitTurn } from './admission-gate.mjs';
9
+
10
+ // A settled turn's snapshot cleanup waits at most 2^5 times its first delay between attempts.
11
+ const SNAPSHOT_BACKOFF_STEPS = 5;
12
+
13
+ /**
14
+ * A managed worktree for `branch` without moving the session (`git
15
+ * worktree add`): retained by this session like a branch it selected, so
16
+ * entering it later (selectBranch) reuses it. A branch already checked out
17
+ * elsewhere is WORKTREE_BRANCH_OCCUPIED, except in this session's own
18
+ * managed worktree, which is returned as it is. `startPoint` (with
19
+ * `create`): the commit the new branch starts at.
20
+ */
21
+ async function addBranchWorktree(
22
+ { hostedSession, sessionProject, backend, runtimeToken, newTxId },
23
+ { branch, create = false, startPoint = null, confirmLarge = false, txId = '', repoCommonDir = null } = {}
24
+ ) {
25
+ const session = hostedSession(null);
26
+ const { workspace } = await sessionProject(session, repoCommonDir);
27
+ const backendApi = await backend();
28
+ const { occupancy } = await backendApi.listBranchOccupancy({
29
+ projectPath: workspace.projectPath,
30
+ sessionId: session.id,
31
+ });
32
+ const held = occupancy.find((row) => row.branch === branch && row.kind !== 'free');
33
+ if (held && !(held.kind === 'session' && held.sessionId === session.id)) {
34
+ throw worktreeError('WORKTREE_BRANCH_OCCUPIED', `Branch ${branch} is checked out in ${held.path}.`, {
35
+ branch,
36
+ path: held.path,
37
+ kind: held.kind,
38
+ ...(held.sessionId ? { sessionId: held.sessionId } : {}),
39
+ });
40
+ }
41
+ const resolved = await backendApi.resolveBranchWorkspace({
42
+ projectPath: workspace.projectPath,
43
+ branch,
44
+ create: create === true,
45
+ startPoint,
46
+ sessionId: session.id,
47
+ runtimeToken,
48
+ txId: String(txId || '').trim() || newTxId(),
49
+ confirmLarge: confirmLarge === true,
50
+ });
51
+ return {
52
+ path: resolved.path,
53
+ branch,
54
+ worktreeId: resolved.entry?.id ?? null,
55
+ created: resolved.created === true,
56
+ baseCommit: resolved.entry?.baseCommit ?? null,
57
+ mergeTarget: resolved.entry?.mergeTarget ?? null,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Remove this session's snapshot at `path` now; a dirty one only with
63
+ * `force`, and none while a worker or background shell runs in it.
64
+ */
65
+ async function removeSnapshot({ hostedSession, backend }, { path, force = false } = {}) {
66
+ const session = hostedSession(null);
67
+ const backendApi = await backend();
68
+ const entry = await backendApi.getWorktreeByPath(path);
69
+ if (entry?.kind !== WORKTREE_SNAPSHOT_KIND) throw worktreeError('WORKTREE_MISSING', undefined, { path });
70
+ if (entry.holderSessionId !== session.id) {
71
+ throw worktreeError(
72
+ 'WORKTREE_BUSY_JOBS',
73
+ "This snapshot belongs to another session's turn; it is removed when that turn ends, or by a prune.",
74
+ { reason: 'snapshot-held', sessionId: entry.holderSessionId, entryId: entry.id }
75
+ );
76
+ }
77
+ const removed = await backendApi.removeSnapshotWorktree({ entryId: entry.id, force: force === true });
78
+ return { ...removed, entryId: entry.id, path: entry.path };
79
+ }
80
+
81
+ /**
82
+ * @param {object} deps The session view (hostedSession, sessionProject), the
83
+ * `backend()` loader, the runtime identity and the snapshot retry bounds.
84
+ */
85
+ export function createSideCheckouts({
86
+ getSession,
87
+ getCurrentCwd,
88
+ hostedSession,
89
+ sessionProject,
90
+ backend,
91
+ runtimeToken,
92
+ newTxId,
93
+ snapshotRetryMs,
94
+ snapshotRetryLimit,
95
+ }) {
96
+ // The creation txIds of the read-only snapshots the current admitted turn
97
+ // made. Each turn owns its own set, so its cleanup never touches a later
98
+ // turn's snapshots (a txId, unlike a freed slug, is never reused).
99
+ let turnSnapshots = null;
100
+ // The running cleanups of settled turns' snapshots (controller → session id).
101
+ const snapshotCleanups = new Map();
102
+ let disposed = false;
103
+
104
+ /**
105
+ * Turn admission (see admission-gate admitTurn); resolves the release. The
106
+ * release also starts the removal of the read-only snapshots this turn
107
+ * made — only those (removeTurnSnapshots).
108
+ */
109
+ async function admitTurnWorkspace({ signal } = {}) {
110
+ const release = await admitTurn({ sessionId: getSession()?.id || '', cwd: () => getCurrentCwd() || '', signal });
111
+ const made = new Set();
112
+ turnSnapshots = made;
113
+ return () => {
114
+ release();
115
+ if (turnSnapshots === made) turnSnapshots = null;
116
+ const sessionId = getSession()?.id;
117
+ if (made.size && sessionId) void removeTurnSnapshots(sessionId, [...made]);
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Remove a settled turn's snapshots (by creation `txIds`) once nothing runs
123
+ * in them: a worker turn or background shell holding a workspace lease
124
+ * inside one keeps it (manager removeSessionSnapshots reports it `in-use`).
125
+ * Those are retried with a doubling delay (`snapshotRetryMs`, at most
126
+ * 2^SNAPSHOT_BACKOFF_STEPS times it) for at most `snapshotRetryLimit`
127
+ * attempts; a snapshot removal that fails otherwise is not retried. What is
128
+ * left then — or when the runtime is disposed, the session closes, is
129
+ * deleted or is no longer the one hosted here — is the prune's and the
130
+ * startup reconcile's. Admission is not held meanwhile: the removal only
131
+ * ever names this turn's snapshots.
132
+ */
133
+ async function removeTurnSnapshots(sessionId, txIds) {
134
+ if (disposed) return;
135
+ const controller = new AbortController();
136
+ snapshotCleanups.set(controller, sessionId);
137
+ try {
138
+ let pending = txIds;
139
+ for (let attempt = 0; pending.length && attempt < snapshotRetryLimit; attempt++) {
140
+ if (attempt > 0) {
141
+ const wait = snapshotRetryMs * 2 ** Math.min(attempt - 1, SNAPSHOT_BACKOFF_STEPS);
142
+ await delay(wait, undefined, { ref: false, signal: controller.signal });
143
+ }
144
+ const session = getSession();
145
+ if (session?.id !== sessionId || session.closed === true) return;
146
+ const report = await (await backend()).removeSessionSnapshots(sessionId, { txIds: pending });
147
+ pending = report.kept.filter((row) => row.reason === 'in-use').map((row) => row.txId);
148
+ }
149
+ } catch {
150
+ // Cancelled, or the backend failed: the rest is the maintenance's.
151
+ } finally {
152
+ snapshotCleanups.delete(controller);
153
+ }
154
+ }
155
+
156
+ /** Stop the snapshot cleanups of `sessionId` (every session's when null). */
157
+ function cancelSnapshotCleanups(sessionId = null) {
158
+ for (const [controller, owner] of snapshotCleanups) {
159
+ if (sessionId === null || owner === sessionId) controller.abort();
160
+ }
161
+ }
162
+
163
+ /** Runtime teardown: no snapshot cleanup runs on (prune / startup reconcile finish them). */
164
+ function dispose() {
165
+ disposed = true;
166
+ cancelSnapshotCleanups();
167
+ }
168
+
169
+ /**
170
+ * A read-only snapshot of `commit` for this session's turn (`git worktree
171
+ * add --detach`): a detached managed checkout no session works in,
172
+ * removed when the turn settles (and nothing runs in it any more). The
173
+ * session does not move. Only inside an admitted turn: a snapshot always
174
+ * belongs to the turn that made it.
175
+ */
176
+ async function createSnapshot({ commit = 'HEAD', confirmLarge = false, txId = '', repoCommonDir = null } = {}) {
177
+ const session = hostedSession(null);
178
+ const made = turnSnapshots;
179
+ if (!made) throw new Error('A read-only snapshot belongs to a turn; none is running in this session.');
180
+ const { workspace } = await sessionProject(session, repoCommonDir);
181
+ const tx = String(txId || '').trim() || newTxId();
182
+ const entry = await (await backend()).createSnapshotWorktree({
183
+ projectPath: workspace.projectPath,
184
+ sessionId: session.id,
185
+ runtimeToken,
186
+ commit,
187
+ txId: tx,
188
+ confirmLarge: confirmLarge === true,
189
+ });
190
+ made.add(tx);
191
+ return entry;
192
+ }
193
+
194
+ return {
195
+ admitTurnWorkspace,
196
+ addBranchWorktree: (input) =>
197
+ addBranchWorktree({ hostedSession, sessionProject, backend, runtimeToken, newTxId }, input),
198
+ createSnapshot,
199
+ removeSnapshot: (input) => removeSnapshot({ hostedSession, backend }, input),
200
+ cancelSnapshotCleanups,
201
+ dispose,
202
+ };
203
+ }