mixdog 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/references/actions.md +5 -2
- package/src/defaults/skills/visualize/SKILL.md +157 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/help.mjs +2 -0
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -1
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/shared/atomic-file.mjs +8 -2
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -118
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -2
- package/src/runtime/shared/provider-api-key.mjs +21 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-surface.mjs +7 -0
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/boot/apis.mjs +35 -0
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +20 -0
- package/src/session-runtime/boot/infrastructure.mjs +1 -0
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +11 -6
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/notification-bus.mjs +9 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/resource-extension-sync.mjs +30 -7
- package/src/session-runtime/resource-mcp-api.mjs +20 -7
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +11 -4
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +4 -1
- package/src/standalone/daemon.mjs +139 -5
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +16 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +35 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/dist/index.mjs +332 -36
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +17 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +2 -0
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +15 -1
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +39 -10
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Lead session workspace transition authority. Every workspace change of
|
|
3
|
+
* the Lead this runtime hosts — entering its retained worktree (created lazily
|
|
4
|
+
* on the first message of a worktree task), returning to a project-folder
|
|
5
|
+
* branch, removing the worktree — goes through applyWorkspaceTransition: one
|
|
6
|
+
* serialized, revisioned commit that retargets the runtime cwd (and the turn's
|
|
7
|
+
* cwd override), stores the durable `workspace` record, re-roots the prompt
|
|
8
|
+
* context of the Lead and its idle workers, and persists. Failures are typed
|
|
9
|
+
* WorktreeErrors and leave the previous workspace intact.
|
|
10
|
+
*
|
|
11
|
+
* Git and registry work is delegated to the managed-worktree backend
|
|
12
|
+
* (runtime/shared/worktrees/manager.mjs), loaded on first use.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import {
|
|
16
|
+
branchMenuEntries,
|
|
17
|
+
isWorktreeBound,
|
|
18
|
+
isWorktreeError,
|
|
19
|
+
normalizeSessionWorkspace,
|
|
20
|
+
worktreeError,
|
|
21
|
+
worktreeNameError,
|
|
22
|
+
} from '../../runtime/shared/worktrees/contract.mjs';
|
|
23
|
+
import { normalizeWorktreeSettings } from '../../runtime/shared/worktrees/settings.mjs';
|
|
24
|
+
import * as maintenance from '../../runtime/shared/worktrees/maintenance.mjs';
|
|
25
|
+
import { inspectRepo, throwIfAborted } from '../../runtime/shared/worktrees/git.mjs';
|
|
26
|
+
import { isWithin, samePath } from '../../runtime/shared/worktrees/paths.mjs';
|
|
27
|
+
import { sessionHasConversationMessages } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
|
|
28
|
+
import { buildWorkspacePromptContext } from '../../runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs';
|
|
29
|
+
import {
|
|
30
|
+
promptContentText,
|
|
31
|
+
refreshSessionWorkspaceContext,
|
|
32
|
+
} from '../../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
33
|
+
import { AGENT_OWNER } from '../../runtime/agent/orchestrator/agent-owner.mjs';
|
|
34
|
+
import { _liveSessions } from '../../runtime/agent/orchestrator/session/store/live-state.mjs';
|
|
35
|
+
import { admitTurn, beginWorkspaceTransition, holdCheckoutFolder } from './admission-gate.mjs';
|
|
36
|
+
import { assertMergeTarget, createProjectBranchOps } from './project-branches.mjs';
|
|
37
|
+
import { assertRetainedFits, createTransitionSteps, undoAll } from './transition-steps.mjs';
|
|
38
|
+
import { readSessionWorkspace } from './workspace-state.mjs';
|
|
39
|
+
|
|
40
|
+
const ACTIVE_STAGES = new Set(['connecting', 'requesting', 'streaming', 'tool_running', 'running', 'cancelling']);
|
|
41
|
+
const BUSY_WORKER_STATES = new Set([...ACTIVE_STAGES, 'starting', 'spawning', 'queued', 'pending']);
|
|
42
|
+
const BUSY_JOB_STATES = new Set(['starting', 'spawning', 'queued', 'pending', 'running']);
|
|
43
|
+
|
|
44
|
+
const loadDefaultManager = () => import('../../runtime/shared/worktrees/manager.mjs');
|
|
45
|
+
|
|
46
|
+
/** Running background shell jobs owned by any of `sessionIds` (shell-jobs.mjs ownership). */
|
|
47
|
+
async function defaultOwnShellJobCount(sessionIds) {
|
|
48
|
+
const { otherSessionBackgroundShellJobCount } = await import(
|
|
49
|
+
'../../runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs'
|
|
50
|
+
);
|
|
51
|
+
return otherSessionBackgroundShellJobCount(new Set()) - otherSessionBackgroundShellJobCount(new Set(sessionIds));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lower = (value) => String(value || '').toLowerCase();
|
|
55
|
+
|
|
56
|
+
/** The registered Project (projects.json) at `path`, or null. */
|
|
57
|
+
async function defaultRegisteredProject(path) {
|
|
58
|
+
const { listProjectsStrict } = await import('../../runtime/shared/projects.mjs');
|
|
59
|
+
return listProjectsStrict().find((entry) => samePath(entry.path, path))?.path ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Re-root one session's prompt at `cwd`: environment facts and instruction
|
|
64
|
+
* files for every session, project memory for the Lead only (workers never
|
|
65
|
+
* carried it).
|
|
66
|
+
*/
|
|
67
|
+
export async function refreshWorkspacePrompt(session, cwd, { loadCoreMemoryContext } = {}) {
|
|
68
|
+
const { environmentContext, instructionFilesContext } = buildWorkspacePromptContext(session, cwd);
|
|
69
|
+
const coreMemoryContext =
|
|
70
|
+
session?.owner === AGENT_OWNER || typeof loadCoreMemoryContext !== 'function'
|
|
71
|
+
? undefined
|
|
72
|
+
: await loadCoreMemoryContext();
|
|
73
|
+
return refreshSessionWorkspaceContext(session, {
|
|
74
|
+
cwd,
|
|
75
|
+
environmentContext,
|
|
76
|
+
coreMemoryContext,
|
|
77
|
+
instructionFilesContext,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creation-time intent: `{ mode: 'worktree', name }` asks for a worktree;
|
|
83
|
+
* `{ mode: 'default' }` — an interactive Lead the user starts (TUI launch) —
|
|
84
|
+
* follows the settings `newTaskDefault`; `{ mode: 'branch' }` or no intent
|
|
85
|
+
* runs in the project folder. Desktop tasks carry their choice from the
|
|
86
|
+
* renderer, and agents, schedules, webhooks and resumed sessions pass none,
|
|
87
|
+
* so the default never applies to them.
|
|
88
|
+
*/
|
|
89
|
+
function creationIntent(worktree, settings) {
|
|
90
|
+
if (worktree?.mode === 'worktree') {
|
|
91
|
+
return { name: worktree.name ?? null, confirmLarge: false, explicit: true, sessionId: null, txId: null };
|
|
92
|
+
}
|
|
93
|
+
return worktree?.mode === 'default' && settings.newTaskDefault === 'worktree'
|
|
94
|
+
? { name: null, confirmLarge: false, explicit: false, sessionId: null, txId: null }
|
|
95
|
+
: null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validName(name) {
|
|
99
|
+
if (name === undefined || name === null) return null;
|
|
100
|
+
const error = worktreeNameError(name);
|
|
101
|
+
if (error) throw new TypeError(`worktree name is invalid: ${error}`);
|
|
102
|
+
return name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeTarget(target) {
|
|
106
|
+
const kind = target?.kind;
|
|
107
|
+
if (kind === 'worktree') return { kind, name: validName(target.name) };
|
|
108
|
+
if (kind === 'branch') {
|
|
109
|
+
if (typeof target.branch !== 'string' || !target.branch.trim()) {
|
|
110
|
+
throw new TypeError('workspace target branch must be a non-empty string.');
|
|
111
|
+
}
|
|
112
|
+
return { kind, branch: target.branch.trim(), create: target.create === true };
|
|
113
|
+
}
|
|
114
|
+
if (kind === 'remove-worktree') return { kind, force: target.force === true };
|
|
115
|
+
throw new TypeError('workspace target kind must be branch, worktree or remove-worktree.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validRevision(value) {
|
|
119
|
+
if (value === undefined || value === null) return undefined;
|
|
120
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
121
|
+
throw new TypeError('expectedRevision must be a non-negative integer.');
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** WORKTREE_REVISION_CONFLICT when the caller expected another workspace revision. */
|
|
127
|
+
function assertRevision(revision, expectedRevision) {
|
|
128
|
+
if (expectedRevision !== undefined && expectedRevision !== revision) {
|
|
129
|
+
throw worktreeError('WORKTREE_REVISION_CONFLICT', undefined, { expectedRevision, revision });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The in-flight marker state of each transition kind. */
|
|
134
|
+
const TRANSITION_STATE = { worktree: 'provisioning', branch: 'switching', 'remove-worktree': 'removing' };
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {object} deps
|
|
138
|
+
* @param {() => any} deps.getSession The Lead session this runtime hosts.
|
|
139
|
+
* @param {() => string} deps.getCurrentCwd
|
|
140
|
+
* @param {(cwd: string) => unknown} deps.applyResolvedCwd Retargets rt cwd, session.cwd and the cwd override.
|
|
141
|
+
* @param {(session: any) => unknown} deps.persistSession
|
|
142
|
+
* @param {(session: any, cwd: string) => Promise<unknown>} deps.refreshWorkspaceContext
|
|
143
|
+
* @param {() => number} deps.getActiveTurnCount
|
|
144
|
+
* @param {() => { agentWorkers?: any[], agentJobs?: any[] }} deps.agentStatusState
|
|
145
|
+
* @param {(sessionId: string, cwd: string) => unknown} [deps.updateLeadWorkerCwd] Moves the Lead's pool row (lead-workers.json).
|
|
146
|
+
*/
|
|
147
|
+
export function createWorkspaceTransitions({
|
|
148
|
+
getSession,
|
|
149
|
+
getReservedSessionId = () => null,
|
|
150
|
+
getCurrentCwd,
|
|
151
|
+
getDesktopSession = () => null,
|
|
152
|
+
getConfig = () => null,
|
|
153
|
+
createCurrentSession = null,
|
|
154
|
+
applyResolvedCwd,
|
|
155
|
+
persistSession,
|
|
156
|
+
refreshWorkspaceContext,
|
|
157
|
+
invalidateCaches = () => {},
|
|
158
|
+
getActiveTurnCount,
|
|
159
|
+
agentStatusState,
|
|
160
|
+
updateLeadWorkerCwd = () => {},
|
|
161
|
+
initialIntent = undefined,
|
|
162
|
+
manager = null,
|
|
163
|
+
loadManager = loadDefaultManager,
|
|
164
|
+
countOwnShellJobs = defaultOwnShellJobCount,
|
|
165
|
+
listLiveSessions = () => [..._liveSessions.values()],
|
|
166
|
+
sessionManager = null,
|
|
167
|
+
inspectProject = inspectRepo,
|
|
168
|
+
findRegisteredProject = defaultRegisteredProject,
|
|
169
|
+
listStoredSessionIds = maintenance.storedSessionIds,
|
|
170
|
+
runtimeToken = randomUUID(),
|
|
171
|
+
newTxId = randomUUID,
|
|
172
|
+
}) {
|
|
173
|
+
const worktreeSettings = () => normalizeWorktreeSettings(getConfig?.()?.worktree);
|
|
174
|
+
let pendingIntent = creationIntent(initialIntent, worktreeSettings());
|
|
175
|
+
let managerPromise = null;
|
|
176
|
+
const backend = () => {
|
|
177
|
+
if (manager) return Promise.resolve(manager);
|
|
178
|
+
managerPromise ||= loadManager();
|
|
179
|
+
return managerPromise;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
function hostedSession(sessionRef) {
|
|
183
|
+
const session = getSession();
|
|
184
|
+
const requested = String(sessionRef?.sessionId || '').trim();
|
|
185
|
+
if (!session?.id || (requested && requested !== session.id)) {
|
|
186
|
+
throw new Error(`workspace: session ${requested || '(none)'} is not hosted by this runtime.`);
|
|
187
|
+
}
|
|
188
|
+
return session;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function ensureSession() {
|
|
192
|
+
if (!getSession()?.id && typeof createCurrentSession === 'function') await createCurrentSession('workspace');
|
|
193
|
+
return hostedSession(null);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function projectPathOf(session) {
|
|
197
|
+
const desktop = getDesktopSession?.() || session?.desktopSession;
|
|
198
|
+
if (desktop?.classification === 'project' && desktop.projectPath) return desktop.projectPath;
|
|
199
|
+
return session?.cwd || getCurrentCwd();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The stored record, or the project-folder workspace (revision 0) of a session that never moved. */
|
|
203
|
+
function currentWorkspace(session) {
|
|
204
|
+
const stored = readSessionWorkspace(session);
|
|
205
|
+
if (stored) return stored;
|
|
206
|
+
const projectPath = projectPathOf(session);
|
|
207
|
+
return normalizeSessionWorkspace({ projectPath, cwd: getCurrentCwd() || projectPath, revision: 0 });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isSessionActive(session) {
|
|
211
|
+
const runtime = sessionManager?.getSessionRuntime?.(session.id);
|
|
212
|
+
if (runtime?.controller?.signal && !runtime.controller.signal.aborted) return true;
|
|
213
|
+
return ACTIVE_STAGES.has(lower(runtime?.stage || session.status));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const workersOf = (session) =>
|
|
217
|
+
listLiveSessions().filter(
|
|
218
|
+
(candidate) =>
|
|
219
|
+
candidate?.id &&
|
|
220
|
+
candidate.id !== session.id &&
|
|
221
|
+
candidate.owner === AGENT_OWNER &&
|
|
222
|
+
candidate.closed !== true &&
|
|
223
|
+
(candidate.ownerSessionId === session.id || candidate.parentSessionId === session.id)
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
/** WORKTREE_BUSY_JOBS while this Lead has a turn, workers or background shells running. */
|
|
227
|
+
async function assertLeadIdle(session, { ownTurn = false } = {}) {
|
|
228
|
+
if (!ownTurn && getActiveTurnCount() > 0) {
|
|
229
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'The session is running a turn; wait for it to finish.', {
|
|
230
|
+
reason: 'turn',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const status = agentStatusState?.() || {};
|
|
234
|
+
const workers = Array.isArray(status.agentWorkers) ? status.agentWorkers : [];
|
|
235
|
+
const jobs = Array.isArray(status.agentJobs) ? status.agentJobs : [];
|
|
236
|
+
const busyWorkers = workers.filter(
|
|
237
|
+
(row) => BUSY_WORKER_STATES.has(lower(row?.status)) || BUSY_WORKER_STATES.has(lower(row?.stage))
|
|
238
|
+
).length;
|
|
239
|
+
const busyJobs = jobs.filter((row) => BUSY_JOB_STATES.has(lower(row?.status))).length;
|
|
240
|
+
if (busyWorkers || busyJobs) {
|
|
241
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', undefined, { reason: 'workers', workers: busyWorkers, jobs: busyJobs });
|
|
242
|
+
}
|
|
243
|
+
const ids = new Set([session.id, ...workers.map((row) => row?.sessionId).filter(Boolean)]);
|
|
244
|
+
for (const worker of workersOf(session)) ids.add(worker.id);
|
|
245
|
+
const shellJobs = await countOwnShellJobs([...ids]);
|
|
246
|
+
if (shellJobs > 0) throw worktreeError('WORKTREE_BUSY_JOBS', undefined, { reason: 'shell-jobs', shellJobs });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* A checkout or merge in the shared checkout `folder` refuses while another
|
|
251
|
+
* session (not this Lead's own workers) works there, then holds the folder
|
|
252
|
+
* until the returned release: other sessions' turns that arrive meanwhile
|
|
253
|
+
* wait (admission gate). `sessionId` null: a session-less (draft) change.
|
|
254
|
+
* @returns {() => void}
|
|
255
|
+
*/
|
|
256
|
+
function holdProjectFolder(sessionId, folder) {
|
|
257
|
+
const users = listLiveSessions().filter(
|
|
258
|
+
(candidate) =>
|
|
259
|
+
candidate?.id &&
|
|
260
|
+
candidate.id !== sessionId &&
|
|
261
|
+
candidate.closed !== true &&
|
|
262
|
+
candidate.remoteAttached !== true &&
|
|
263
|
+
(!sessionId || (candidate.ownerSessionId !== sessionId && candidate.parentSessionId !== sessionId)) &&
|
|
264
|
+
typeof candidate.cwd === 'string' &&
|
|
265
|
+
isWithin(folder, candidate.cwd) &&
|
|
266
|
+
isSessionActive(candidate)
|
|
267
|
+
);
|
|
268
|
+
if (users.length) {
|
|
269
|
+
throw worktreeError('WORKTREE_BUSY_JOBS', 'Other sessions are working in the project folder.', {
|
|
270
|
+
reason: 'folder-in-use',
|
|
271
|
+
sessions: users.map((candidate) => candidate.id),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return holdCheckoutFolder(sessionId, folder);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Run `fn` holding the checkout `folder` (holdProjectFolder); released when it settles. */
|
|
278
|
+
async function withProjectFolder(sessionId, folder, fn) {
|
|
279
|
+
const release = holdProjectFolder(sessionId, folder);
|
|
280
|
+
try {
|
|
281
|
+
return await fn();
|
|
282
|
+
} finally {
|
|
283
|
+
release();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const steps = createTransitionSteps({
|
|
288
|
+
backend,
|
|
289
|
+
inspectProject,
|
|
290
|
+
runtimeToken,
|
|
291
|
+
getCurrentCwd,
|
|
292
|
+
applyResolvedCwd,
|
|
293
|
+
persistSession,
|
|
294
|
+
refreshWorkspaceContext,
|
|
295
|
+
invalidateCaches,
|
|
296
|
+
updateLeadWorkerCwd,
|
|
297
|
+
workersOf,
|
|
298
|
+
withProjectFolder,
|
|
299
|
+
});
|
|
300
|
+
const projectBranches = createProjectBranchOps({
|
|
301
|
+
backend,
|
|
302
|
+
inspectProject,
|
|
303
|
+
findRegisteredProject,
|
|
304
|
+
withProjectFolder,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
async function transition(session, target, options = {}) {
|
|
308
|
+
throwIfAborted(options.signal);
|
|
309
|
+
const previous = currentWorkspace(session);
|
|
310
|
+
assertRevision(previous.revision, options.expectedRevision);
|
|
311
|
+
const txId = String(options.txId || '').trim() || newTxId();
|
|
312
|
+
const leave = beginWorkspaceTransition(session.id, txId);
|
|
313
|
+
const hadRecord = Object.hasOwn(session, 'workspace');
|
|
314
|
+
const stored = session.workspace;
|
|
315
|
+
const marker = normalizeSessionWorkspace({
|
|
316
|
+
...previous,
|
|
317
|
+
transition: { state: TRANSITION_STATE[target.kind], txId },
|
|
318
|
+
});
|
|
319
|
+
session.workspace = marker;
|
|
320
|
+
const restore = () => {
|
|
321
|
+
if (hadRecord) session.workspace = stored;
|
|
322
|
+
else delete session.workspace;
|
|
323
|
+
};
|
|
324
|
+
const journal = [];
|
|
325
|
+
try {
|
|
326
|
+
await assertLeadIdle(session, { ownTurn: options.ownTurn === true });
|
|
327
|
+
const run = { signal: options.signal, txId, journal };
|
|
328
|
+
if (target.kind === 'worktree') {
|
|
329
|
+
return await steps.enterWorktree(session, previous, {
|
|
330
|
+
...run,
|
|
331
|
+
name: target.name,
|
|
332
|
+
nameHint: options.nameHint,
|
|
333
|
+
confirmLarge: options.confirmLarge,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (target.kind === 'branch') {
|
|
337
|
+
return await steps.enterProjectBranch(session, previous, {
|
|
338
|
+
...run,
|
|
339
|
+
branch: target.branch,
|
|
340
|
+
create: target.create,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return await steps.removeRetainedWorktree(session, previous, { ...run, force: target.force });
|
|
344
|
+
} catch (error) {
|
|
345
|
+
// Whatever already moved (cwd, record, prompts, worker roots, lease)
|
|
346
|
+
// goes back exactly; the previous workspace stays authoritative.
|
|
347
|
+
await undoAll(journal, error);
|
|
348
|
+
throw error;
|
|
349
|
+
} finally {
|
|
350
|
+
if (session.workspace === marker) restore();
|
|
351
|
+
leave();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** @type {import('../../runtime/shared/worktrees/contract.mjs').ApplyWorkspaceTransition} */
|
|
356
|
+
async function applyWorkspaceTransition(sessionRef, target, options = {}) {
|
|
357
|
+
const session = hostedSession(sessionRef);
|
|
358
|
+
return transition(session, normalizeTarget(target), {
|
|
359
|
+
expectedRevision: validRevision(options.expectedRevision),
|
|
360
|
+
confirmLarge: options.confirmLarge === true,
|
|
361
|
+
signal: options.signal,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The pending worktree intent that applies to `session`'s next turn, if any. */
|
|
366
|
+
function intentFor(session) {
|
|
367
|
+
const intent = pendingIntent;
|
|
368
|
+
if (!intent) return null;
|
|
369
|
+
if (isWorktreeBound(readSessionWorkspace(session))) {
|
|
370
|
+
pendingIntent = null;
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
if (intent.sessionId) return intent.sessionId === session.id ? intent : null;
|
|
374
|
+
// A creation-time intent belongs to a new task: never to a resumed
|
|
375
|
+
// conversation this runtime opened instead.
|
|
376
|
+
const started =
|
|
377
|
+
sessionHasConversationMessages(session) || sessionHasConversationMessages({ messages: session.liveTurnMessages });
|
|
378
|
+
return started ? null : intent;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Create and enter the worktree `intent` asks for now: its txId is fixed on
|
|
383
|
+
* first use (`submissionId`, else a fresh one) so a retry reuses the same
|
|
384
|
+
* worktree, and the prompt text names an unnamed one. A settled intent is
|
|
385
|
+
* consumed.
|
|
386
|
+
*/
|
|
387
|
+
async function provisionIntent(session, intent, submissionId, prompt, options) {
|
|
388
|
+
intent.txId ||= String(submissionId || '').trim() || newTxId();
|
|
389
|
+
const result = await transition(
|
|
390
|
+
session,
|
|
391
|
+
{ kind: 'worktree', name: intent.name },
|
|
392
|
+
{ txId: intent.txId, nameHint: promptContentText(prompt), confirmLarge: intent.confirmLarge, ...options }
|
|
393
|
+
);
|
|
394
|
+
if (pendingIntent === intent) pendingIntent = null;
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Turn-start workspace gate, run after the session exists and before the
|
|
400
|
+
* user row, the review snapshot, hooks, the provider and any worker spawn.
|
|
401
|
+
* A session in recovery refuses the turn; a pending worktree intent is
|
|
402
|
+
* provisioned now (txId = the first submission id, reused by retries; an
|
|
403
|
+
* unnamed worktree takes its slug from the first message).
|
|
404
|
+
*/
|
|
405
|
+
async function prepareTurnWorkspace({ submissionId = '', prompt = '', signal, confirmLarge = false } = {}) {
|
|
406
|
+
const session = getSession();
|
|
407
|
+
if (!session?.id) return null;
|
|
408
|
+
const stored = readSessionWorkspace(session);
|
|
409
|
+
if (stored?.transition.state === 'recovery') {
|
|
410
|
+
throw worktreeError(
|
|
411
|
+
'WORKTREE_MISSING',
|
|
412
|
+
'The worktree for this session no longer exists; pick a branch or remove the worktree to continue.',
|
|
413
|
+
{ worktreeId: stored.worktreeId, path: stored.cwd }
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const intent = intentFor(session);
|
|
417
|
+
if (!intent) return null;
|
|
418
|
+
try {
|
|
419
|
+
return await provisionIntent(session, intent, submissionId, prompt, {
|
|
420
|
+
confirmLarge: intent.confirmLarge || confirmLarge === true,
|
|
421
|
+
signal,
|
|
422
|
+
ownTurn: true,
|
|
423
|
+
});
|
|
424
|
+
} catch (error) {
|
|
425
|
+
// The settings default never blocks a task outside a git repository.
|
|
426
|
+
if (!intent.explicit && isWorktreeError(error, 'WORKTREE_UNSUPPORTED_REPO')) {
|
|
427
|
+
if (pendingIntent === intent) pendingIntent = null;
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function getBranchMenu() {
|
|
435
|
+
const session = getSession();
|
|
436
|
+
const workspace = session?.id
|
|
437
|
+
? currentWorkspace(session)
|
|
438
|
+
: normalizeSessionWorkspace({ projectPath: projectPathOf(null), cwd: getCurrentCwd(), revision: 0 });
|
|
439
|
+
const menu = await (await backend()).listBranchOccupancy({
|
|
440
|
+
projectPath: workspace.projectPath,
|
|
441
|
+
sessionId: session?.id || null,
|
|
442
|
+
});
|
|
443
|
+
// A session in the project folder (never moved, or back from a worktree)
|
|
444
|
+
// is on whatever that folder has checked out now.
|
|
445
|
+
const checkedOut = menu.occupancy.find((row) => row.kind === 'current')?.branch ?? null;
|
|
446
|
+
return {
|
|
447
|
+
workspace: isWorktreeBound(workspace) || !checkedOut ? workspace : { ...workspace, branch: checkedOut },
|
|
448
|
+
pendingWorktree: pendingIntent ? { name: pendingIntent.name } : null,
|
|
449
|
+
branches: branchMenuEntries(menu.branches, menu.occupancy),
|
|
450
|
+
occupancy: menu.occupancy,
|
|
451
|
+
retainedWorktree: menu.retainedWorktree ?? null,
|
|
452
|
+
// Creation notes of the retained worktree (submodules, include entries).
|
|
453
|
+
warnings: Array.isArray(menu.retainedWorktree?.warnings) ? [...menu.retainedWorktree.warnings] : [],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The maintenance scope here: every live session of this process counts as a live owner. */
|
|
458
|
+
async function maintenanceScope() {
|
|
459
|
+
const liveSessionIds = [
|
|
460
|
+
...new Set([getSession()?.id, ...listLiveSessions().map((candidate) => candidate?.id)].filter(Boolean)),
|
|
461
|
+
];
|
|
462
|
+
return { liveSessionIds, listSessionIds: listStoredSessionIds, manager: await backend() };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Every managed worktree with who retains it (the manual list). */
|
|
466
|
+
async function listWorktrees() {
|
|
467
|
+
return maintenance.listWorktreesWithRetention(await maintenanceScope());
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** The manual prune: clean, merged worktrees no stored or live session retains. */
|
|
471
|
+
async function pruneWorktrees() {
|
|
472
|
+
return maintenance.pruneWorktrees(await maintenanceScope());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** After `sessionId` was deleted: autoCleanup removes its clean, merged worktree. */
|
|
476
|
+
async function cleanupDeletedSession(sessionId) {
|
|
477
|
+
return maintenance.cleanupDeletedSessionWorktree(sessionId, {
|
|
478
|
+
manager: await backend(),
|
|
479
|
+
autoCleanup: worktreeSettings().autoCleanup,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Turn admission (see admission-gate admitTurn); resolves the release. */
|
|
484
|
+
function admitTurnWorkspace({ signal } = {}) {
|
|
485
|
+
return admitTurn({ sessionId: getSession()?.id || '', cwd: () => getCurrentCwd() || '', signal });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function selectBranch({ branch, create = false, expectedRevision, confirmLarge = false } = {}) {
|
|
489
|
+
const session = await ensureSession();
|
|
490
|
+
const retained = await (await backend()).getSessionWorktree(session.id);
|
|
491
|
+
const target =
|
|
492
|
+
retained && retained.branch === branch && create !== true
|
|
493
|
+
? { kind: 'worktree' }
|
|
494
|
+
: { kind: 'branch', branch, create: create === true };
|
|
495
|
+
const result = await applyWorkspaceTransition({ sessionId: session.id }, target, {
|
|
496
|
+
expectedRevision,
|
|
497
|
+
confirmLarge,
|
|
498
|
+
});
|
|
499
|
+
pendingIntent = null;
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* "워크트리": re-enter the retained worktree now, or arm the lazy creation
|
|
505
|
+
* for the next message (confirmLarge acknowledges WORKTREE_LARGE_REPO_CONFIRM).
|
|
506
|
+
*
|
|
507
|
+
* `provision` (a New Task's first send, before its prompt is submitted)
|
|
508
|
+
* creates and enters the worktree right away instead: `prompt` names it
|
|
509
|
+
* like the lazy path would and `txId` (the submission id) makes a resend
|
|
510
|
+
* reuse it. Typed failures (large repository, disk, busy) therefore reach
|
|
511
|
+
* the sender while its draft is still unsent, and the turn that follows
|
|
512
|
+
* finds the session already bound.
|
|
513
|
+
*/
|
|
514
|
+
async function requestWorktree({
|
|
515
|
+
name = null,
|
|
516
|
+
confirmLarge = false,
|
|
517
|
+
provision = false,
|
|
518
|
+
prompt = '',
|
|
519
|
+
txId = '',
|
|
520
|
+
expectedRevision,
|
|
521
|
+
} = {}) {
|
|
522
|
+
const nameHint = validName(name);
|
|
523
|
+
const expected = validRevision(expectedRevision);
|
|
524
|
+
const session = provision === true ? await ensureSession() : getSession();
|
|
525
|
+
// No session yet: the menu it was read from had revision 0.
|
|
526
|
+
const current = session?.id ? currentWorkspace(session) : null;
|
|
527
|
+
assertRevision(current?.revision ?? 0, expected);
|
|
528
|
+
if (current) {
|
|
529
|
+
if (isWorktreeBound(current)) return { revision: current.revision, workspace: current, pending: false };
|
|
530
|
+
if (await (await backend()).getSessionWorktree(session.id)) {
|
|
531
|
+
const entered = await applyWorkspaceTransition(
|
|
532
|
+
{ sessionId: session.id },
|
|
533
|
+
{ kind: 'worktree' },
|
|
534
|
+
{ expectedRevision: expected }
|
|
535
|
+
);
|
|
536
|
+
return { ...entered, pending: false };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
pendingIntent = {
|
|
540
|
+
name: nameHint ?? pendingIntent?.name ?? null,
|
|
541
|
+
confirmLarge: confirmLarge === true || pendingIntent?.confirmLarge === true,
|
|
542
|
+
explicit: true,
|
|
543
|
+
sessionId: session?.id || getReservedSessionId?.() || null,
|
|
544
|
+
txId: pendingIntent?.txId ?? null,
|
|
545
|
+
};
|
|
546
|
+
if (provision === true) {
|
|
547
|
+
return {
|
|
548
|
+
...(await provisionIntent(session, pendingIntent, txId, prompt, { expectedRevision: expected })),
|
|
549
|
+
pending: false,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const workspace = session?.id ? currentWorkspace(session) : null;
|
|
553
|
+
return { revision: workspace?.revision ?? 0, workspace, pending: true };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Merge the retained worktree into the project folder's checked-out branch.
|
|
558
|
+
* `ownTurn` (internal: the Lead's own tool call) tolerates the calling turn.
|
|
559
|
+
*/
|
|
560
|
+
async function mergeWorkspace(
|
|
561
|
+
{ confirmed = false, target = null, cleanup = false, expectedRevision } = {},
|
|
562
|
+
{ ownTurn = false } = {}
|
|
563
|
+
) {
|
|
564
|
+
if (confirmed !== true) {
|
|
565
|
+
throw worktreeError('WORKTREE_CONFIRM_REQUIRED', 'mergeWorkspace requires confirmed: true.');
|
|
566
|
+
}
|
|
567
|
+
const expected = validRevision(expectedRevision);
|
|
568
|
+
const session = hostedSession(null);
|
|
569
|
+
const merged = await mergeRetainedWorktree(session, { target, ownTurn, expectedRevision: expected });
|
|
570
|
+
if (cleanup !== true || merged.merged !== true) return merged;
|
|
571
|
+
// "Merge and clean up": with autoCleanup on, the merged worktree goes now
|
|
572
|
+
// (a dirty one stays, reported); off, nothing is removed automatically.
|
|
573
|
+
if (worktreeSettings().autoCleanup !== true) {
|
|
574
|
+
return { ...merged, cleanup: { removed: false, reason: 'auto-cleanup-off' } };
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const removed = await removeWorktree({ force: false }, { ownTurn });
|
|
578
|
+
return { ...merged, cleanup: removed.removed, revision: removed.revision, workspace: removed.workspace };
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (!isWorktreeError(error, 'WORKTREE_DIRTY') && !isWorktreeError(error, 'WORKTREE_UNMERGED')) throw error;
|
|
581
|
+
return { ...merged, cleanup: { removed: false, reason: error.code } };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* The merge itself. The retained worktree must belong to this session's
|
|
587
|
+
* Project (same repository and Project folder — a Project switch may have
|
|
588
|
+
* left it behind): its recorded project folder is the destination that is
|
|
589
|
+
* guarded, compared with the confirmed target and merged into.
|
|
590
|
+
*/
|
|
591
|
+
async function mergeRetainedWorktree(session, { target, ownTurn, expectedRevision }) {
|
|
592
|
+
const workspace = currentWorkspace(session);
|
|
593
|
+
assertRevision(workspace.revision, expectedRevision);
|
|
594
|
+
const leave = beginWorkspaceTransition(session.id, `merge:${newTxId()}`);
|
|
595
|
+
try {
|
|
596
|
+
await assertLeadIdle(session, { ownTurn: ownTurn === true });
|
|
597
|
+
const backendApi = await backend();
|
|
598
|
+
const entry = await backendApi.getSessionWorktree(session.id);
|
|
599
|
+
if (!entry) throw worktreeError('WORKTREE_MISSING', 'This session has no worktree to merge.');
|
|
600
|
+
const project = await inspectProject(workspace.projectPath);
|
|
601
|
+
assertRetainedFits(entry, project, workspace.projectPath);
|
|
602
|
+
if (!samePath(entry.projectPath, workspace.projectPath)) {
|
|
603
|
+
throw worktreeError('WORKTREE_PROJECT_MISMATCH', undefined, {
|
|
604
|
+
worktreeId: entry.id,
|
|
605
|
+
worktreeProject: entry.projectPath,
|
|
606
|
+
projectPath: workspace.projectPath,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
assertMergeTarget(project.currentBranch, target);
|
|
610
|
+
return await withProjectFolder(session.id, project.topLevel || workspace.projectPath, () =>
|
|
611
|
+
backendApi.mergeWorktreeIntoProject({
|
|
612
|
+
entryId: entry.id,
|
|
613
|
+
target: project.currentBranch,
|
|
614
|
+
confirmed: true,
|
|
615
|
+
projectPath: entry.projectPath,
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
} finally {
|
|
619
|
+
leave();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function removeWorktree({ force = false, expectedRevision } = {}, { ownTurn = false } = {}) {
|
|
624
|
+
const expected = validRevision(expectedRevision);
|
|
625
|
+
const session = hostedSession(null);
|
|
626
|
+
return transition(
|
|
627
|
+
session,
|
|
628
|
+
{ kind: 'remove-worktree', force: force === true },
|
|
629
|
+
{ ownTurn: ownTurn === true, expectedRevision: expected }
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
applyWorkspaceTransition,
|
|
635
|
+
prepareTurnWorkspace,
|
|
636
|
+
admitTurnWorkspace,
|
|
637
|
+
/** The durable workspace record of the hosted session (null: project folder, never moved). */
|
|
638
|
+
getWorkspace: () => getSession()?.workspace ?? null,
|
|
639
|
+
getBranchMenu,
|
|
640
|
+
getProjectBranchMenu: projectBranches.getProjectBranchMenu,
|
|
641
|
+
selectBranch,
|
|
642
|
+
selectProjectBranch: projectBranches.selectProjectBranch,
|
|
643
|
+
mergeProjectBranch: projectBranches.mergeProjectBranch,
|
|
644
|
+
requestWorktree,
|
|
645
|
+
mergeWorkspace,
|
|
646
|
+
removeWorktree,
|
|
647
|
+
listWorktrees,
|
|
648
|
+
pruneWorktrees,
|
|
649
|
+
cleanupDeletedSession,
|
|
650
|
+
};
|
|
651
|
+
}
|