mixdog 1.2.1 → 1.2.3
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 +18 -0
- package/package.json +2 -1
- package/scripts/gate-local.mjs +19 -3
- package/scripts/model-catalog-audit.mjs +1 -15
- package/scripts/provider-pingpong-live.mjs +509 -0
- package/src/agents/advisor/AGENT.md +1 -1
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +3 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/SKILL.md +6 -1
- package/src/defaults/skills/setup/references/actions.md +44 -22
- package/src/defaults/skills/setup/references/surfaces.md +10 -8
- package/src/headless-exec.mjs +15 -8
- package/src/lib/keychain-cjs.cjs +7 -4
- package/src/output-styles/common.md +11 -10
- package/src/repl/slash-commands.mjs +16 -1
- package/src/rules/agent/AGENT.md +5 -5
- package/src/rules/lead/LEAD.md +5 -5
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +92 -61
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +10 -1
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +7 -0
- package/src/runtime/agent/orchestrator/config.mjs +5 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +26 -7
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +112 -7
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +48 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +48 -37
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +10 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +102 -45
- package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-blocks.mjs +20 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-delta.mjs +15 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/message-events.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +12 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-outcome.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-state.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +40 -7
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +88 -21
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +30 -10
- package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +106 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +10 -61
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +94 -15
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +117 -72
- package/src/runtime/agent/orchestrator/providers/compat-request-policy.mjs +78 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +75 -10
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +103 -23
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-messages.mjs +43 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +32 -6
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +27 -3
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +64 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +70 -6
- package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +34 -7
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +32 -6
- package/src/runtime/agent/orchestrator/providers/deepseek.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +80 -18
- package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +46 -8
- package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +273 -0
- package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +41 -5
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +369 -112
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +140 -35
- package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +70 -24
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +80 -29
- 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/grok-oauth.mjs +69 -19
- package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +60 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +246 -42
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-server-tool-usage.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-system-blocks.mjs +27 -6
- package/src/runtime/agent/orchestrator/providers/lib/claude-bridge-schema.mjs +388 -0
- package/src/runtime/agent/orchestrator/providers/lib/inclusive-usage.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/lib/origin-bound-fetch.mjs +16 -0
- package/src/runtime/agent/orchestrator/providers/lib/responses-terminal-fields.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/lib/wire-pairing.mjs +48 -22
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +119 -18
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +13 -15
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +1 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +33 -1
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +123 -61
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +35 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +27 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +37 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/tool-call-acc.mjs +2 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +47 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +47 -25
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-reasoning-field.mjs +151 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +64 -23
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +79 -33
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-head-parity.json +2901 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-parity.fixtures.mjs +172 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +144 -15
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +109 -26
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +27 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +34 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +23 -4
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +23 -10
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +43 -15
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-items.mjs +18 -7
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +7 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/text-relay.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
- package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +80 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +37 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +76 -33
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload-parity.fixture.json +323 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +44 -16
- package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +7 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/refusals.mjs +60 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/tool-calls.mjs +51 -35
- package/src/runtime/agent/orchestrator/providers/openai-ws-response-state.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/terminal-frames.mjs +25 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +41 -22
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +170 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +75 -70
- package/src/runtime/agent/orchestrator/providers/openrouter.mjs +75 -0
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +720 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +123 -219
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
- package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
- package/src/runtime/agent/orchestrator/providers/transport-scope.d.mts +6 -0
- package/src/runtime/agent/orchestrator/providers/transport-scope.mjs +28 -0
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +157 -0
- package/src/runtime/agent/orchestrator/providers/wire-route.mjs +512 -0
- package/src/runtime/agent/orchestrator/providers/xai-tool-search-head-parity.json +1131 -0
- package/src/runtime/agent/orchestrator/providers/xai-tool-search.fixtures.mjs +183 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +100 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-availability.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
- package/src/runtime/agent/orchestrator/runtime-core/provider-request-snapshot.mjs +7 -3
- package/src/runtime/agent/orchestrator/runtime-core/session-title.mjs +21 -1
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +14 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +526 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +10 -7
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +176 -71
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +62 -10
- package/src/runtime/agent/orchestrator/runtime-core/tool-exposure.mjs +96 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +14 -93
- package/src/runtime/agent/orchestrator/session/compact/summary-anchor.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +120 -0
- package/src/runtime/agent/orchestrator/session/context-tool-schema.mjs +17 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +8 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +91 -1
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +56 -0
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +21 -10
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +132 -0
- package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +30 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +67 -20
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +104 -19
- package/src/runtime/agent/orchestrator/session/loop/tool-call-resolution.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +30 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +15 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +150 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +34 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +57 -21
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +57 -25
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +208 -14
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +16 -4
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +25 -3
- package/src/runtime/agent/orchestrator/session/manager/session-fallback-title.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -2
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +168 -29
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +96 -0
- package/src/runtime/agent/orchestrator/session/side-question.mjs +192 -0
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +17 -3
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +58 -2
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +41 -28
- package/src/runtime/agent/orchestrator/session/store/session-observers.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/store/write-admission.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
- package/src/runtime/agent/orchestrator/session/store-summary-archived-agent.mjs +34 -15
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/store.mjs +19 -3
- package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-schema-rejection.mjs +77 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +14 -10
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +49 -34
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +73 -39
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +18 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -4
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +88 -12
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +2 -0
- package/src/runtime/computer-bridge/action-schema.mjs +44 -21
- package/src/runtime/github/tool.mjs +92 -2
- package/src/runtime/memory/index.mjs +1 -0
- package/src/runtime/memory/lib/http-router/ingest-routes.mjs +22 -1
- package/src/runtime/memory/lib/ingest/transcript-offsets.mjs +2 -0
- package/src/runtime/memory/lib/pg/adapter.mjs +98 -1
- package/src/runtime/memory/lib/pg/process.mjs +65 -50
- package/src/runtime/memory/lib/pg/run-child.mjs +49 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +124 -16
- package/src/runtime/memory/lib/runtime-fetcher.mjs +12 -13
- package/src/runtime/memory/lib/transcript-ingest.mjs +14 -0
- package/src/runtime/office/README.md +2 -2
- package/src/runtime/office/authoring/html-document-author.mjs +2 -0
- package/src/runtime/office/authoring/pptx-contact-sheet.mjs +2 -0
- package/src/runtime/office/authoring/pptx-html-measure.mjs +2 -0
- package/src/runtime/office/authoring/pptx-html-runner.mjs +2 -0
- package/src/runtime/office/authoring/xlsx-html-runner.mjs +2 -0
- package/src/runtime/office/core/office-actions-read.mjs +3 -0
- package/src/runtime/office/core/office-render-preview.mjs +2 -0
- package/src/runtime/office/core/office-session-dispatch.mjs +7 -0
- package/src/runtime/office/core/office-sessionless-actions.mjs +8 -0
- package/src/runtime/office/core/office-sessions.mjs +5 -0
- package/src/runtime/office/core/office-transactions.mjs +3 -0
- package/src/runtime/office/core/office-write-ledger.mjs +98 -0
- package/src/runtime/office/design/library/templates/mixdog-executive.mixdog-edit.pptx +0 -0
- package/src/runtime/office/index.mjs +30 -0
- package/src/runtime/office/pdf/pdf-edit-document.mjs +2 -0
- package/src/runtime/office/pdf/pdf-render.mjs +23 -7
- package/src/runtime/office/pdf/pdf-security.mjs +2 -0
- package/src/runtime/office/portable/portable-opc.mjs +4 -1
- package/src/runtime/office/quality/visual-diff.mjs +2 -0
- package/src/runtime/office/shared/values.mjs +0 -5
- package/src/runtime/shared/background-tasks.mjs +92 -14
- package/src/runtime/shared/bounded-file-read.mjs +13 -0
- package/src/runtime/shared/bounded-line-diff.mjs +182 -0
- package/src/runtime/shared/buffered-appender.mjs +18 -0
- package/src/runtime/shared/config.mjs +78 -18
- package/src/runtime/shared/err-text.mjs +31 -0
- package/src/runtime/shared/error-presentation.mjs +159 -19
- package/src/runtime/shared/file-permissions.d.mts +6 -1
- package/src/runtime/shared/file-permissions.mjs +54 -6
- package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
- package/src/runtime/shared/llm/anthropic-effort.mjs +129 -134
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
- package/src/runtime/shared/llm/model-catalog.mjs +53 -40
- package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
- package/src/runtime/shared/llm/usage-accounting.mjs +5 -2
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +3 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
- package/src/runtime/shared/loopback-listener.mjs +9 -0
- package/src/runtime/shared/model-display.mjs +13 -0
- package/src/runtime/shared/provider-api-key.mjs +2 -0
- package/src/runtime/shared/quota-resume-store.mjs +11 -0
- package/src/runtime/shared/review-diff.mjs +52 -0
- package/src/runtime/shared/session-baseline-store.mjs +960 -0
- package/src/runtime/shared/session-sidebar-host.d.mts +4 -0
- package/src/runtime/shared/session-sidebar-host.mjs +38 -0
- package/src/runtime/shared/slow-span.mjs +114 -0
- package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
- package/src/runtime/shared/statusline/statusline-segments.mjs +28 -4
- package/src/runtime/shared/task-notification-envelope.mjs +2 -2
- package/src/runtime/shared/tool-card-model/card-header.mjs +6 -1
- package/src/runtime/shared/tool-card-model/generic-detail.mjs +39 -5
- package/src/runtime/shared/tool-card-model.mjs +5 -2
- package/src/runtime/shared/tool-output-limit.mjs +4 -0
- package/src/runtime/shared/tool-primitives.mjs +13 -0
- package/src/runtime/shared/tool-result-summary.mjs +8 -1
- package/src/runtime/shared/tool-surface.mjs +18 -3
- package/src/runtime/shared/tool-work-units.mjs +40 -4
- package/src/runtime/shared/transcript-writer/purge.mjs +74 -0
- package/src/runtime/shared/transcript-writer.mjs +10 -2
- package/src/runtime/shared/turn-changes.mjs +670 -274
- package/src/runtime/shared/turn-review-reply.mjs +8 -39
- package/src/runtime/shared/turn-snapshot-store.mjs +94 -2
- package/src/runtime/shared/turn-snapshot.mjs +986 -690
- package/src/runtime/shared/turn-worktree-snapshot.mjs +279 -262
- package/src/runtime/shared/user-data-guard.mjs +262 -49
- package/src/runtime/shared/worktrees/carry.mjs +531 -0
- package/src/runtime/shared/worktrees/contract.mjs +34 -24
- package/src/runtime/shared/worktrees/file-recovery.mjs +68 -0
- package/src/runtime/shared/worktrees/git-head.mjs +205 -0
- package/src/runtime/shared/worktrees/git.mjs +8 -3
- package/src/runtime/shared/worktrees/maintenance.mjs +1 -1
- package/src/runtime/shared/worktrees/manager.mjs +156 -8
- package/src/runtime/shared/worktrees/merge.mjs +31 -21
- package/src/runtime/shared/worktrees/paths.d.mts +6 -0
- package/src/runtime/shared/worktrees/test-support.mjs +19 -4
- package/src/runtime/terminal-bridge/client.mjs +44 -11
- package/src/runtime/terminal-bridge/tool-defs.mjs +13 -6
- package/src/runtime/tidy/apply.mjs +47 -2
- package/src/runtime/tidy/run-engines.mjs +11 -5
- package/src/session-runtime/agent-tool-routing.mjs +19 -13
- package/src/session-runtime/boot/apis.mjs +19 -5
- package/src/session-runtime/boot/facade.mjs +1 -0
- package/src/session-runtime/boot/infrastructure.mjs +12 -1
- package/src/session-runtime/boot/session.mjs +0 -1
- package/src/session-runtime/boot/tools.mjs +4 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +12 -5
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +65 -0
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +3 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +11 -4
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +115 -18
- package/src/session-runtime/internal-tool-executor.mjs +6 -2
- package/src/session-runtime/lifecycle/cold-heir.mjs +188 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +71 -27
- package/src/session-runtime/lifecycle/session-catalog.mjs +14 -3
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +6 -0
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +29 -6
- package/src/session-runtime/lifecycle/teardown.mjs +73 -8
- package/src/session-runtime/lifecycle-api.mjs +1 -0
- package/src/session-runtime/mcp-glue/connect.mjs +46 -14
- package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
- package/src/session-runtime/mcp-glue.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +174 -0
- package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
- package/src/session-runtime/model-route/set-route.mjs +85 -17
- package/src/session-runtime/model-route/web-search-route.mjs +1 -1
- package/src/session-runtime/native-web-search/routes.mjs +22 -3
- package/src/session-runtime/native-web-search-usage.mjs +51 -0
- package/src/session-runtime/native-web-search.mjs +10 -1
- package/src/session-runtime/new-session-config.mjs +4 -2
- package/src/session-runtime/provider-auth/login.mjs +4 -0
- package/src/session-runtime/provider-models/model-meta.mjs +12 -3
- package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
- package/src/session-runtime/quick-web-search-models.mjs +13 -38
- package/src/session-runtime/resource-extension-sync.mjs +4 -0
- package/src/session-runtime/resource-mcp-api.mjs +98 -2
- package/src/session-runtime/resource-scope-decorate.mjs +10 -3
- package/src/session-runtime/runtime-review-api.mjs +21 -2
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +81 -20
- package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
- package/src/session-runtime/services/agent-tool/lead-worker-index/forgotten-leads.mjs +29 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index/index-file.mjs +1 -1
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +73 -7
- package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +15 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +10 -1
- package/src/session-runtime/services/agent-tool/worker-index.mjs +46 -7
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
- package/src/session-runtime/services/agent-tool.mjs +3 -0
- package/src/session-runtime/services/channel-admin.mjs +19 -3
- package/src/session-runtime/services/memory-runtime-proxy/calls.mjs +4 -0
- package/src/session-runtime/services/memory-runtime-proxy.mjs +1 -0
- package/src/session-runtime/services/provider-admin.mjs +81 -15
- package/src/session-runtime/session-transcript-purge.mjs +32 -0
- package/src/session-runtime/session-turn-api.mjs +14 -5
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -0
- package/src/session-runtime/settings-compaction-api.mjs +1 -0
- package/src/session-runtime/setup-tool/executor.mjs +36 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +86 -6
- package/src/session-runtime/setup-tool/settings-contract.mjs +13 -3
- package/src/session-runtime/setup-tool/tool-defs.mjs +13 -4
- package/src/session-runtime/skills-api.mjs +3 -5
- package/src/session-runtime/tool-surface.mjs +5 -1
- package/src/session-runtime/turn/compaction-gate.mjs +25 -0
- package/src/session-runtime/turn/session-ops.mjs +7 -5
- package/src/session-runtime/turn/side-question-ops.mjs +22 -0
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +5 -1
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +8 -3
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +10 -0
- package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
- package/src/session-runtime/turn/turn-run.mjs +25 -9
- package/src/session-runtime/usage-stats-api.mjs +43 -11
- package/src/session-runtime/workspace/project-branches.mjs +7 -5
- package/src/session-runtime/workspace/transition-steps.mjs +129 -31
- package/src/session-runtime/workspace/transitions.mjs +190 -15
- package/src/standalone/daemon-agent-control.mjs +9 -1
- package/src/standalone/daemon-drain-policy.mjs +8 -0
- package/src/standalone/daemon-local-session-bridge.mjs +14 -1
- package/src/standalone/daemon-owner.mjs +17 -4
- package/src/standalone/daemon-pg-stop.mjs +51 -0
- package/src/standalone/daemon-stop-budget.d.mts +2 -0
- package/src/standalone/daemon-stop-budget.mjs +18 -0
- package/src/standalone/daemon-stored-session-views.mjs +2 -0
- package/src/standalone/daemon-telemetry.mjs +74 -0
- package/src/standalone/daemon.mjs +64 -8
- package/src/standalone/desktop-service-registry.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +70 -19
- package/src/standalone/session-client.mjs +42 -0
- package/src/standalone/session-protocol.mjs +14 -0
- package/src/standalone/session-proxy/projection.mjs +41 -4
- package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +15 -1
- package/src/standalone/session-runtime-inline-host.mjs +1 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
- package/src/standalone/session-service/agent-tree.mjs +12 -0
- package/src/standalone/session-service/quota-resume.mjs +8 -0
- package/src/standalone/session-service/retention/busy.mjs +15 -1
- package/src/standalone/session-service/session-calls/session-action.mjs +18 -3
- package/src/standalone/session-service-api.mjs +11 -1
- package/src/standalone/session-service.mjs +1 -0
- package/src/standalone/session-state-patch.d.mts +2 -1
- package/src/standalone/session-state-patch.mjs +41 -1
- package/src/tui/App.jsx +25 -0
- package/src/tui/app/app-view/floating-panel.jsx +26 -1
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/btw-controller.mjs +118 -0
- package/src/tui/app/create-app-pickers.mjs +6 -0
- package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
- package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
- package/src/tui/app/extension-pickers.mjs +2 -1
- package/src/tui/app/model-options.mjs +11 -16
- package/src/tui/app/shell-layout/row-budgets.mjs +11 -1
- package/src/tui/app/shell-layout/surfaces.mjs +3 -1
- package/src/tui/app/slash-commands.mjs +20 -5
- package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
- package/src/tui/app/slash-dispatch/session-commands.mjs +19 -6
- package/src/tui/app/use-global-key-input/approval-shortcut.mjs +12 -0
- package/src/tui/app/use-global-key-input.mjs +5 -12
- package/src/tui/app/use-prompt-handlers.mjs +11 -0
- package/src/tui/components/BtwPanel.jsx +53 -0
- package/src/tui/components/QuestionPanel.jsx +275 -0
- package/src/tui/components/StatusLine.jsx +2 -0
- package/src/tui/components/question-panel-model.mjs +156 -0
- package/src/tui/components/status-line/local-line.mjs +5 -2
- package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
- package/src/tui/dist/index.mjs +3221 -1500
- package/src/tui/session/context-state.mjs +4 -2
- package/src/tui/session/quota-resume.mjs +46 -14
- package/src/tui/session/session-api/commands.mjs +43 -24
- package/src/tui/session/session-api/controls.mjs +2 -2
- package/src/tui/session/session-api/extensions.mjs +22 -0
- package/src/tui/session/session-api/integrations.mjs +16 -2
- package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +7 -2
- package/src/tui/session/session-api/routes.mjs +71 -7
- package/src/tui/session/session-api/settings.mjs +11 -14
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/session-flow/drain.mjs +7 -1
- package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
- package/src/tui/session/tool-approval.mjs +10 -7
- package/src/tui/session/transcript-spill.mjs +14 -7
- package/src/tui/session/turn-stream.mjs +174 -31
- package/src/tui/session/turn.mjs +46 -16
- package/src/tui/statusline-ansi-bridge.mjs +1 -1
- package/src/ui/statusline-agents.mjs +15 -4
- package/src/ui/statusline.mjs +7 -3
- package/src/ui/streaming-markdown-heal.mjs +34 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
- package/src/workflows/default/WORKFLOW.md +3 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
- package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project checkout's uncommitted changes and their move into a new managed
|
|
3
|
+
* worktree (manager.mjs createManagedWorktree runs it under the repository
|
|
4
|
+
* lock, before the include entries are copied).
|
|
5
|
+
*
|
|
6
|
+
* One scope everywhere: the WHOLE repository checkout (a Project in a
|
|
7
|
+
* subfolder included) — detection, snapshot, apply and clear all cover every
|
|
8
|
+
* tracked and untracked path of the checkout, because the worktree is a full
|
|
9
|
+
* checkout too. Only an untracked session identity file
|
|
10
|
+
* (<project>/.mixdog/project.id, Mixdog metadata) is left out. Ignored files
|
|
11
|
+
* (dependencies, build output, env files) are never carried: they stay in
|
|
12
|
+
* the project folder.
|
|
13
|
+
*
|
|
14
|
+
* A snapshot is two trees taken through a temporary copy of the index, so
|
|
15
|
+
* the checkout is never touched while it is read: `index` (what is staged)
|
|
16
|
+
* and `tree` (the working tree: tracked changes, deletions and untracked
|
|
17
|
+
* files). The carry, in order:
|
|
18
|
+
* 1. refuse what cannot move faithfully (assertCarryable: an operation in
|
|
19
|
+
* progress, unmerged entries, dirty submodules, nested repositories,
|
|
20
|
+
* filtered paths such as Git LFS, untracked files above the size limit);
|
|
21
|
+
* 2. snapshot the project checkout and keep it at a named recovery ref
|
|
22
|
+
* (refs/mixdog/carry/<id>-<time>: a commit of the working tree whose
|
|
23
|
+
* first parent is HEAD and whose second parent is the commit of the index);
|
|
24
|
+
* 3. write it into the fresh worktree and verify the worktree snapshots
|
|
25
|
+
* identically;
|
|
26
|
+
* 4. re-snapshot the project checkout (unchanged since 2, else refuse), then
|
|
27
|
+
* clear it: `reset --hard` and delete exactly its untracked files; verify
|
|
28
|
+
* it is clean.
|
|
29
|
+
* A failure restores the project checkout from the snapshot and reports
|
|
30
|
+
* WORKTREE_DIRTY (reason `carry-failed`) with the kept ref. A success
|
|
31
|
+
* returns the ref too: the caller drops it (dropRecoveryRef) only once the
|
|
32
|
+
* whole session move is committed, and restores from it
|
|
33
|
+
* (restoreFromRecoveryRef) if anything after the carry fails.
|
|
34
|
+
*
|
|
35
|
+
* Sessions are kept out by the caller (the project folder is held and must
|
|
36
|
+
* be quiet), but a process outside Mixdog — an editor, a file watcher — can
|
|
37
|
+
* still write between the final comparison and the clear; such a write is
|
|
38
|
+
* not captured.
|
|
39
|
+
*/
|
|
40
|
+
import { randomUUID } from 'node:crypto';
|
|
41
|
+
import { existsSync } from 'node:fs';
|
|
42
|
+
import { copyFile, lstat, rm, rmdir } from 'node:fs/promises';
|
|
43
|
+
import { dirname, join } from 'node:path';
|
|
44
|
+
import { worktreeError } from './contract.mjs';
|
|
45
|
+
import { firstLine, gitStatusRunner, runGit } from './git.mjs';
|
|
46
|
+
import { operationInProgress } from './merge.mjs';
|
|
47
|
+
|
|
48
|
+
const IDENTITY_PATH = '.mixdog/project.id';
|
|
49
|
+
const RECOVERY_REF_PREFIX = 'refs/mixdog/carry/';
|
|
50
|
+
/** Untracked files beyond these limits are refused rather than snapshotted. */
|
|
51
|
+
export const CARRY_UNTRACKED_MAX_BYTES = 256 * 1024 * 1024;
|
|
52
|
+
export const CARRY_UNTRACKED_MAX_FILES = 10_000;
|
|
53
|
+
const CHECK_ATTR_CHUNK = 200;
|
|
54
|
+
// Recovery commits are Mixdog's own, independent of the user's identity config.
|
|
55
|
+
const COMMIT_ENV = Object.freeze({
|
|
56
|
+
GIT_AUTHOR_NAME: 'Mixdog',
|
|
57
|
+
GIT_AUTHOR_EMAIL: 'mixdog@localhost',
|
|
58
|
+
GIT_COMMITTER_NAME: 'Mixdog',
|
|
59
|
+
GIT_COMMITTER_EMAIL: 'mixdog@localhost',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function identityPath(projectSubdir) {
|
|
63
|
+
return projectSubdir ? `${projectSubdir}/${IDENTITY_PATH}` : IDENTITY_PATH;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** CE_INTENT_TO_ADD in an index entry's flags (`git add -N`). */
|
|
67
|
+
const INTENT_TO_ADD_FLAG = 0x20000000;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `git status --porcelain=v2 -z` rows: path, untracked, unmerged, submodule.
|
|
71
|
+
* @returns {Array<{ path: string, untracked: boolean, unmerged: boolean, submodule: boolean }>}
|
|
72
|
+
*/
|
|
73
|
+
function parseStatusV2(raw) {
|
|
74
|
+
const fields = String(raw || '').split('\0');
|
|
75
|
+
const rows = [];
|
|
76
|
+
for (let index = 0; index < fields.length; index++) {
|
|
77
|
+
const field = fields[index];
|
|
78
|
+
if (!field) continue;
|
|
79
|
+
const kind = field[0];
|
|
80
|
+
if (kind === '?') {
|
|
81
|
+
rows.push({ path: field.slice(2), untracked: true, unmerged: false, submodule: false });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Leading fields before the path: 1 → 8, 2 → 9 (then the original path), u → 10.
|
|
85
|
+
const lead = kind === '1' ? 8 : kind === '2' ? 9 : kind === 'u' ? 10 : -1;
|
|
86
|
+
if (lead < 0) continue;
|
|
87
|
+
const parts = field.split(' ');
|
|
88
|
+
rows.push({
|
|
89
|
+
path: parts.slice(lead).join(' '),
|
|
90
|
+
untracked: false,
|
|
91
|
+
unmerged: kind === 'u',
|
|
92
|
+
submodule: String(parts[2] || '').startsWith('S'),
|
|
93
|
+
});
|
|
94
|
+
if (kind === '2') index++;
|
|
95
|
+
}
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* One `git status` read of the checkout (the single source for everything
|
|
101
|
+
* the carry asks of it): the changed rows, their paths, the untracked
|
|
102
|
+
* (not ignored) files — nested repositories end in "/" — and `identity`,
|
|
103
|
+
* the untracked-and-not-ignored session identity file's path (left out of
|
|
104
|
+
* every step; status lists exactly such a file), else null.
|
|
105
|
+
*/
|
|
106
|
+
async function inspectCheckout(topLevel, projectSubdir) {
|
|
107
|
+
const status = await runGit(topLevel, ['status', '--porcelain=v2', '-z', '--untracked-files=all'], {
|
|
108
|
+
readOnly: true,
|
|
109
|
+
});
|
|
110
|
+
const identity = identityPath(projectSubdir);
|
|
111
|
+
const all = parseStatusV2(status.stdout);
|
|
112
|
+
const rows = all.filter((row) => !(row.untracked && row.path === identity));
|
|
113
|
+
return {
|
|
114
|
+
rows,
|
|
115
|
+
files: rows.map((row) => row.path),
|
|
116
|
+
untracked: rows.filter((row) => row.untracked).map((row) => row.path),
|
|
117
|
+
identity: rows.length < all.length ? identity : null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Uncommitted paths of the whole checkout at `topLevel`: staged, unstaged
|
|
123
|
+
* and untracked (every file); ignored files and an untracked identity file
|
|
124
|
+
* excluded.
|
|
125
|
+
* @returns {Promise<string[]>}
|
|
126
|
+
*/
|
|
127
|
+
export async function uncommittedChanges(topLevel, projectSubdir = '') {
|
|
128
|
+
return (await inspectCheckout(topLevel, projectSubdir)).files;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function carryRefusal(message, details) {
|
|
132
|
+
return worktreeError(
|
|
133
|
+
'WORKTREE_DIRTY',
|
|
134
|
+
`${message} Leave the changes in the project folder instead, or fix this and retry.`,
|
|
135
|
+
details
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Paths among `paths` with a clean/smudge filter (Git LFS and custom filters). */
|
|
140
|
+
async function filteredPaths(topLevel, paths) {
|
|
141
|
+
const filtered = [];
|
|
142
|
+
for (let start = 0; start < paths.length; start += CHECK_ATTR_CHUNK) {
|
|
143
|
+
const chunk = paths.slice(start, start + CHECK_ATTR_CHUNK);
|
|
144
|
+
const result = await runGit(topLevel, ['check-attr', '-z', 'filter', '--', ...chunk], { readOnly: true });
|
|
145
|
+
const fields = result.stdout.split('\0');
|
|
146
|
+
for (let index = 0; index + 2 < fields.length; index += 3) {
|
|
147
|
+
if (!['unspecified', 'unset'].includes(fields[index + 2])) filtered.push(fields[index]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return filtered;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* WORKTREE_DIRTY (with a `reason`) when the checkout's changes cannot be
|
|
155
|
+
* carried faithfully: a merge, rebase, cherry-pick or revert in progress;
|
|
156
|
+
* a bisect or a pending cherry-pick/revert sequence; a sparse checkout, or
|
|
157
|
+
* index entries marked skip-worktree or assume-unchanged (their edits are
|
|
158
|
+
* invisible to git); intent-to-add entries; unmerged index entries; a
|
|
159
|
+
* submodule with changes; an untracked nested repository; a changed path
|
|
160
|
+
* under a clean/smudge filter (Git LFS); untracked files above
|
|
161
|
+
* CARRY_UNTRACKED_MAX_FILES / CARRY_UNTRACKED_MAX_BYTES.
|
|
162
|
+
* Resolves the checkout's uncommitted paths.
|
|
163
|
+
* @returns {Promise<string[]>}
|
|
164
|
+
*/
|
|
165
|
+
export async function assertCarryable(topLevel, projectSubdir = '') {
|
|
166
|
+
return (await checkCarryable(topLevel, projectSubdir)).files;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Index entries git does not look at (their edits are invisible to status,
|
|
171
|
+
* so a carry would silently drop or leave them): `assumeUnchanged` (`git
|
|
172
|
+
* update-index --assume-unchanged`, a lowercase `ls-files -v` tag),
|
|
173
|
+
* `skipWorktree` (tag S/s: sparse checkouts, `--skip-worktree`), and whether
|
|
174
|
+
* sparse checkout is enabled at all.
|
|
175
|
+
*/
|
|
176
|
+
async function hiddenIndexEntries(topLevel) {
|
|
177
|
+
const [listed, debug, sparse] = await Promise.all([
|
|
178
|
+
runGit(topLevel, ['ls-files', '-v', '-z'], { readOnly: true }),
|
|
179
|
+
runGit(topLevel, ['ls-files', '--debug', '-z'], { readOnly: true }),
|
|
180
|
+
runGit(topLevel, ['config', '--type=bool', '--get', 'core.sparseCheckout'], { readOnly: true, allowFailure: true }),
|
|
181
|
+
]);
|
|
182
|
+
const skipWorktree = [];
|
|
183
|
+
const assumeUnchanged = [];
|
|
184
|
+
for (const entry of listed.stdout.split('\0')) {
|
|
185
|
+
const tag = entry[0];
|
|
186
|
+
if (tag === 'S' || tag === 's') skipWorktree.push(entry.slice(2));
|
|
187
|
+
else if (tag && tag !== tag.toUpperCase()) assumeUnchanged.push(entry.slice(2));
|
|
188
|
+
}
|
|
189
|
+
// `--debug -z` prints "path NUL stat lines ending in `flags: <hex>` LF" per
|
|
190
|
+
// entry; the index's own intent-to-add flag holds whatever the working file
|
|
191
|
+
// does (a deleted one is ".D" in status, not ".A").
|
|
192
|
+
const intentToAdd = [];
|
|
193
|
+
const segments = debug.stdout.split('\0');
|
|
194
|
+
let path = segments[0];
|
|
195
|
+
for (let index = 1; index < segments.length; index++) {
|
|
196
|
+
const block = /^[\s\S]*?flags: ([0-9a-f]+)\n([\s\S]*)$/.exec(segments[index]);
|
|
197
|
+
if (!block) break;
|
|
198
|
+
if (parseInt(block[1], 16) & INTENT_TO_ADD_FLAG) intentToAdd.push(path);
|
|
199
|
+
path = block[2];
|
|
200
|
+
}
|
|
201
|
+
return { skipWorktree, assumeUnchanged, intentToAdd, sparse: sparse.stdout.trim() === 'true' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** assertCarryable's checks; resolves the status read they were made on. */
|
|
205
|
+
async function checkCarryable(topLevel, projectSubdir) {
|
|
206
|
+
const [operation, hidden, checkout] = await Promise.all([
|
|
207
|
+
operationInProgress(gitStatusRunner, topLevel),
|
|
208
|
+
hiddenIndexEntries(topLevel),
|
|
209
|
+
inspectCheckout(topLevel, projectSubdir),
|
|
210
|
+
]);
|
|
211
|
+
if (operation) {
|
|
212
|
+
throw carryRefusal(`A ${operation} is in progress in the project folder; finish or abort it first.`, {
|
|
213
|
+
reason: 'operation-in-progress',
|
|
214
|
+
operation,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (hidden.sparse) {
|
|
218
|
+
throw carryRefusal('The project folder uses a sparse checkout, which a carry cannot reproduce.', {
|
|
219
|
+
reason: 'sparse-checkout',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (hidden.skipWorktree.length) {
|
|
223
|
+
throw carryRefusal('Files marked skip-worktree have changes git does not see, so they cannot be carried.', {
|
|
224
|
+
reason: 'skip-worktree',
|
|
225
|
+
paths: hidden.skipWorktree.slice(0, 50),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (hidden.assumeUnchanged.length) {
|
|
229
|
+
throw carryRefusal('Files marked assume-unchanged have changes git does not see, so they cannot be carried.', {
|
|
230
|
+
reason: 'assume-unchanged',
|
|
231
|
+
paths: hidden.assumeUnchanged.slice(0, 50),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const { rows, untracked } = checkout;
|
|
235
|
+
if (rows.some((row) => row.unmerged)) {
|
|
236
|
+
throw carryRefusal('The project folder has unresolved merge conflicts; resolve them first.', {
|
|
237
|
+
reason: 'unmerged-paths',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const submodules = rows.filter((row) => row.submodule).map((row) => row.path);
|
|
241
|
+
if (submodules.length) {
|
|
242
|
+
throw carryRefusal('Submodules with changes cannot be carried; commit or reset them first.', {
|
|
243
|
+
reason: 'dirty-submodules',
|
|
244
|
+
paths: submodules.slice(0, 50),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const { intentToAdd } = hidden;
|
|
248
|
+
if (intentToAdd.length) {
|
|
249
|
+
throw carryRefusal(
|
|
250
|
+
'Files added with "git add -N" (intent to add) cannot be carried; stage or unstage them first.',
|
|
251
|
+
{
|
|
252
|
+
reason: 'intent-to-add',
|
|
253
|
+
paths: intentToAdd.slice(0, 50),
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const nested = untracked.filter((path) => path.endsWith('/'));
|
|
258
|
+
if (nested.length) {
|
|
259
|
+
throw carryRefusal('The project folder has untracked nested repositories, which cannot be carried.', {
|
|
260
|
+
reason: 'nested-repository',
|
|
261
|
+
paths: nested.slice(0, 50),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (untracked.length > CARRY_UNTRACKED_MAX_FILES) {
|
|
265
|
+
throw carryRefusal(
|
|
266
|
+
`The project folder has ${untracked.length} untracked files (limit ${CARRY_UNTRACKED_MAX_FILES}); ` +
|
|
267
|
+
'add generated output to .gitignore first.',
|
|
268
|
+
{ reason: 'too-large', files: untracked.length, maxFiles: CARRY_UNTRACKED_MAX_FILES }
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
let bytes = 0;
|
|
272
|
+
for (const rel of untracked) {
|
|
273
|
+
bytes += (await lstat(join(topLevel, ...rel.split('/'))).catch(() => null))?.size ?? 0;
|
|
274
|
+
if (bytes > CARRY_UNTRACKED_MAX_BYTES) {
|
|
275
|
+
throw carryRefusal(
|
|
276
|
+
`The project folder's untracked files exceed ${Math.round(CARRY_UNTRACKED_MAX_BYTES / 1048576)} MB; ` +
|
|
277
|
+
'add large or generated files to .gitignore first.',
|
|
278
|
+
{ reason: 'too-large', maxBytes: CARRY_UNTRACKED_MAX_BYTES }
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const filtered = await filteredPaths(
|
|
283
|
+
topLevel,
|
|
284
|
+
rows.map((row) => row.path)
|
|
285
|
+
);
|
|
286
|
+
if (filtered.length) {
|
|
287
|
+
throw carryRefusal('Changed files use a git filter (such as Git LFS), which a carry cannot reproduce exactly.', {
|
|
288
|
+
reason: 'filtered-paths',
|
|
289
|
+
paths: filtered.slice(0, 50),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return checkout;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* { head, index, tree } of the checkout at `cwd`, read through temporary
|
|
297
|
+
* copies of its index: the live index is copied once, and two duplicates of
|
|
298
|
+
* that capture (one stays as staged, one takes `add --all`) are what git
|
|
299
|
+
* works on side by side. `identity` (inspectCheckout's: only ever an
|
|
300
|
+
* untracked, not ignored path — naming an ignored one, even as an exclude,
|
|
301
|
+
* makes `add` refuse) is left out.
|
|
302
|
+
*/
|
|
303
|
+
async function snapshot(cwd, identity) {
|
|
304
|
+
const [head, indexFile] = (
|
|
305
|
+
await runGit(cwd, ['rev-parse', '--path-format=absolute', 'HEAD^{commit}', '--git-path', 'index'])
|
|
306
|
+
).stdout
|
|
307
|
+
.split(/\r?\n/)
|
|
308
|
+
.map((line) => line.trim());
|
|
309
|
+
const temp = `${indexFile}.mixdog-carry-${randomUUID()}`;
|
|
310
|
+
const capturedIndex = `${temp}-captured`;
|
|
311
|
+
const stagedIndex = `${temp}-staged`;
|
|
312
|
+
const treeIndex = `${temp}-tree`;
|
|
313
|
+
const writeTree = async (file) =>
|
|
314
|
+
firstLine((await runGit(cwd, ['write-tree'], { env: { GIT_INDEX_FILE: file } })).stdout);
|
|
315
|
+
const pathspec = identity ? ['--', '.', `:(exclude,literal)${identity}`] : ['--', '.'];
|
|
316
|
+
try {
|
|
317
|
+
// The live index is read once; both working copies come from that capture.
|
|
318
|
+
if (existsSync(indexFile)) await copyFile(indexFile, capturedIndex);
|
|
319
|
+
else await runGit(cwd, ['read-tree', head], { env: { GIT_INDEX_FILE: capturedIndex } });
|
|
320
|
+
await Promise.all([copyFile(capturedIndex, stagedIndex), copyFile(capturedIndex, treeIndex)]);
|
|
321
|
+
const settled = await Promise.allSettled([
|
|
322
|
+
writeTree(stagedIndex),
|
|
323
|
+
runGit(cwd, ['add', '--all', ...pathspec], { env: { GIT_INDEX_FILE: treeIndex } }).then(() =>
|
|
324
|
+
writeTree(treeIndex)
|
|
325
|
+
),
|
|
326
|
+
]);
|
|
327
|
+
const failed = settled.find((result) => result.status === 'rejected');
|
|
328
|
+
if (failed) throw failed.reason;
|
|
329
|
+
return { head, index: settled[0].value, tree: settled[1].value };
|
|
330
|
+
} finally {
|
|
331
|
+
for (const file of [capturedIndex, stagedIndex, treeIndex]) {
|
|
332
|
+
await rm(file, { force: true });
|
|
333
|
+
await rm(`${file}.lock`, { force: true });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const sameSnapshot = (left, right) =>
|
|
339
|
+
!!left && !!right && left.head === right.head && left.index === right.index && left.tree === right.tree;
|
|
340
|
+
|
|
341
|
+
async function keepRecoveryRef(cwd, snap, label) {
|
|
342
|
+
const commit = async (tree, parents, message) =>
|
|
343
|
+
firstLine(
|
|
344
|
+
(
|
|
345
|
+
await runGit(cwd, ['commit-tree', tree, ...parents.flatMap((parent) => ['-p', parent]), '-m', message], {
|
|
346
|
+
env: COMMIT_ENV,
|
|
347
|
+
})
|
|
348
|
+
).stdout
|
|
349
|
+
);
|
|
350
|
+
const indexCommit = await commit(snap.index, [snap.head], `mixdog: staged changes carried into worktree ${label}`);
|
|
351
|
+
const stateCommit = await commit(
|
|
352
|
+
snap.tree,
|
|
353
|
+
[snap.head, indexCommit],
|
|
354
|
+
`mixdog: uncommitted changes (untracked included) carried into worktree ${label}`
|
|
355
|
+
);
|
|
356
|
+
const ref = `${RECOVERY_REF_PREFIX}${label}-${Date.now()}`;
|
|
357
|
+
await runGit(cwd, ['update-ref', '-m', `mixdog: carry into worktree ${label}`, ref, stateCommit]);
|
|
358
|
+
return ref;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Make the checkout at `cwd` (index and files) exactly `snap`; HEAD stays. */
|
|
362
|
+
async function writeSnapshot(cwd, snap, { reset = false } = {}) {
|
|
363
|
+
await runGit(
|
|
364
|
+
cwd,
|
|
365
|
+
reset ? ['read-tree', '--reset', '-u', snap.tree] : ['read-tree', '-u', '-m', snap.head, snap.tree]
|
|
366
|
+
);
|
|
367
|
+
// The index now equals the working tree (`snap.tree`); moving it on to the
|
|
368
|
+
// staged state as a two-tree switch (-i: files are already as wanted) keeps
|
|
369
|
+
// the stat data of every entry that does not change. A plain
|
|
370
|
+
// `read-tree <index>` drops all of it, and the `update-index --refresh` it
|
|
371
|
+
// then needs re-reads every file of the checkout (seconds on a large one).
|
|
372
|
+
await runGit(cwd, ['read-tree', '-m', '-i', snap.tree, snap.index]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Restore `snap` into the project checkout; true once it snapshots exactly as `snap`. */
|
|
376
|
+
async function restoreSnapshot(topLevel, identity, snap) {
|
|
377
|
+
let failure = null;
|
|
378
|
+
try {
|
|
379
|
+
await writeSnapshot(topLevel, snap, { reset: true });
|
|
380
|
+
} catch (error) {
|
|
381
|
+
failure = error;
|
|
382
|
+
}
|
|
383
|
+
// A clear that failed before changing anything needs no restore at all.
|
|
384
|
+
if (sameSnapshot(await snapshot(topLevel, identity).catch(() => null), snap)) return null;
|
|
385
|
+
return failure || new Error('the project folder does not match the snapshot after the restore');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Tracked files back to HEAD, then exactly `untracked` deleted (and the folders they leave empty). */
|
|
389
|
+
async function clearCheckout(topLevel, untracked) {
|
|
390
|
+
await runGit(topLevel, ['reset', '--hard', '--quiet', 'HEAD']);
|
|
391
|
+
const folders = new Set();
|
|
392
|
+
for (const rel of untracked) {
|
|
393
|
+
const path = join(topLevel, ...rel.split('/'));
|
|
394
|
+
await rm(path, { force: true });
|
|
395
|
+
for (let dir = dirname(path); dir.length > topLevel.length && dir.startsWith(topLevel); dir = dirname(dir)) {
|
|
396
|
+
folders.add(dir);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const dir of [...folders].sort((a, b) => b.length - a.length)) {
|
|
400
|
+
await rmdir(dir).catch(() => {});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Write `snap` into the fresh worktree and verify it snapshots identically. */
|
|
405
|
+
async function applyToWorktree(worktreePath, identity, snap) {
|
|
406
|
+
await writeSnapshot(worktreePath, snap);
|
|
407
|
+
if (!sameSnapshot(await snapshot(worktreePath, identity), snap)) {
|
|
408
|
+
throw new Error('the worktree does not match the project folder after the changes were applied');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function recoveryHint(ref) {
|
|
413
|
+
return `"git read-tree --reset -u ${ref}" then "git read-tree ${ref}^2" (the staged state)`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Move the uncommitted changes of the project checkout at `topLevel` into
|
|
418
|
+
* the fresh worktree at `worktreePath` (a checkout of `baseCommit`).
|
|
419
|
+
* Nothing to move resolves `{ count: 0, recoveryRef: null }` without
|
|
420
|
+
* touching either. On success the recovery ref is kept for the caller.
|
|
421
|
+
* @param {{ topLevel: string, projectSubdir?: string, worktreePath: string, baseCommit: string, label: string }} input
|
|
422
|
+
* @returns {Promise<{ count: number, recoveryRef: string|null }>}
|
|
423
|
+
*/
|
|
424
|
+
export async function carryUncommittedChanges({ topLevel, projectSubdir = '', worktreePath, baseCommit, label }) {
|
|
425
|
+
// Checked again right before acting (the worktree took a while to create);
|
|
426
|
+
// the same status read serves the file list, the untracked files to clear
|
|
427
|
+
// and the identity file to leave out.
|
|
428
|
+
const { files, untracked, identity } = await checkCarryable(topLevel, projectSubdir);
|
|
429
|
+
if (!files.length) return { count: 0, recoveryRef: null };
|
|
430
|
+
const snap = await snapshot(topLevel, identity);
|
|
431
|
+
if (snap.head !== baseCommit) {
|
|
432
|
+
throw carryRefusal('The project folder moved to another commit while the worktree was created.', {
|
|
433
|
+
reason: 'head-moved',
|
|
434
|
+
head: snap.head,
|
|
435
|
+
baseCommit,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
// The recovery ref is written while the worktree takes the snapshot; it is
|
|
439
|
+
// awaited before anything in the project folder is touched.
|
|
440
|
+
const keeping = keepRecoveryRef(topLevel, snap, label);
|
|
441
|
+
const applying = applyToWorktree(worktreePath, identity, snap).then(
|
|
442
|
+
() => null,
|
|
443
|
+
(error) => error
|
|
444
|
+
);
|
|
445
|
+
let recoveryRef;
|
|
446
|
+
try {
|
|
447
|
+
recoveryRef = await keeping;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
await applying;
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
let touched = false;
|
|
453
|
+
try {
|
|
454
|
+
const applyError = await applying;
|
|
455
|
+
if (applyError) throw applyError;
|
|
456
|
+
if (!sameSnapshot(await snapshot(topLevel, identity), snap)) {
|
|
457
|
+
throw new Error('the project folder changed while its changes were being moved');
|
|
458
|
+
}
|
|
459
|
+
touched = true;
|
|
460
|
+
await clearCheckout(topLevel, untracked);
|
|
461
|
+
const left = (await inspectCheckout(topLevel, projectSubdir)).files;
|
|
462
|
+
if (left.length) throw new Error(`${left.length} changed path(s) could not be cleared from the project folder`);
|
|
463
|
+
} catch (cause) {
|
|
464
|
+
const restoreError = touched ? await restoreSnapshot(topLevel, identity, snap) : null;
|
|
465
|
+
const reason = String(cause?.message || cause);
|
|
466
|
+
throw worktreeError(
|
|
467
|
+
'WORKTREE_DIRTY',
|
|
468
|
+
restoreError
|
|
469
|
+
? `Moving the uncommitted changes into the worktree failed (${reason}) and the project folder could not be ` +
|
|
470
|
+
`restored; recover them in the project folder with ${recoveryHint(recoveryRef)}.`
|
|
471
|
+
: `Moving the uncommitted changes into the worktree failed (${reason}); the project folder still has them ` +
|
|
472
|
+
`(a copy is kept at ${recoveryRef}).`,
|
|
473
|
+
{
|
|
474
|
+
reason: 'carry-failed',
|
|
475
|
+
recoveryRef,
|
|
476
|
+
restored: !restoreError,
|
|
477
|
+
count: files.length,
|
|
478
|
+
cause: reason,
|
|
479
|
+
...(restoreError ? { restoreError: String(restoreError.message || restoreError) } : {}),
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return { count: files.length, recoveryRef };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function assertRecoveryRef(ref) {
|
|
487
|
+
if (typeof ref !== 'string' || !ref.startsWith(RECOVERY_REF_PREFIX) || ref.includes('..')) {
|
|
488
|
+
throw new TypeError('recoveryRef must be a mixdog carry ref.');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Put the changes kept at `recoveryRef` back into the project checkout
|
|
494
|
+
* after a carry whose session move failed later. Only a checkout still
|
|
495
|
+
* clean and on the ref's base commit is restored (nothing there can be
|
|
496
|
+
* overwritten); otherwise WORKTREE_DIRTY (reason `carry-restore-refused`)
|
|
497
|
+
* and the ref stays. A restored checkout keeps the ref (dropRecoveryRef).
|
|
498
|
+
*/
|
|
499
|
+
export async function restoreFromRecoveryRef(topLevel, projectSubdir, recoveryRef) {
|
|
500
|
+
assertRecoveryRef(recoveryRef);
|
|
501
|
+
const rev = async (spec) => firstLine((await runGit(topLevel, ['rev-parse', '--verify', spec])).stdout);
|
|
502
|
+
const snap = {
|
|
503
|
+
head: await rev(`${recoveryRef}^1`),
|
|
504
|
+
index: await rev(`${recoveryRef}^2^{tree}`),
|
|
505
|
+
tree: await rev(`${recoveryRef}^{tree}`),
|
|
506
|
+
};
|
|
507
|
+
const head = await rev('HEAD^{commit}');
|
|
508
|
+
const { files: dirty, identity } = await inspectCheckout(topLevel, projectSubdir);
|
|
509
|
+
if (head !== snap.head || dirty.length) {
|
|
510
|
+
throw worktreeError(
|
|
511
|
+
'WORKTREE_DIRTY',
|
|
512
|
+
`The carried changes could not be put back: the project folder changed meanwhile. Recover them with ${recoveryHint(recoveryRef)}.`,
|
|
513
|
+
{ reason: 'carry-restore-refused', recoveryRef, count: dirty.length }
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const failure = await restoreSnapshot(topLevel, identity, snap);
|
|
517
|
+
if (failure) {
|
|
518
|
+
throw worktreeError(
|
|
519
|
+
'WORKTREE_DIRTY',
|
|
520
|
+
`The carried changes could not be put back (${failure.message || failure}). Recover them with ${recoveryHint(recoveryRef)}.`,
|
|
521
|
+
{ reason: 'carry-restore-failed', recoveryRef }
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return { restored: true, recoveryRef };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Forget a carry's recovery ref once the move it protected is committed. */
|
|
528
|
+
export async function dropRecoveryRef(topLevel, recoveryRef) {
|
|
529
|
+
assertRecoveryRef(recoveryRef);
|
|
530
|
+
await runGit(topLevel, ['update-ref', '-d', recoveryRef], { allowFailure: true });
|
|
531
|
+
}
|
|
@@ -19,7 +19,6 @@ export const DEFAULT_WORKTREE_BRANCH_PREFIX = 'mixdog/';
|
|
|
19
19
|
|
|
20
20
|
export const WORKSPACE_TRANSITION_STATES = Object.freeze(['idle', 'provisioning', 'switching', 'removing', 'recovery']);
|
|
21
21
|
const WORKTREE_ENTRY_STATES = Object.freeze(['creating', 'ready', 'deleting', 'recovery']);
|
|
22
|
-
const BRANCH_OCCUPANCY_KINDS = Object.freeze(['current', 'session', 'external', 'free']);
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* Stable error codes. Clients branch on `error.code`, never on message text.
|
|
@@ -31,6 +30,8 @@ export const WORKTREE_ERROR_CODES = Object.freeze({
|
|
|
31
30
|
WORKTREE_DISK_INSUFFICIENT: 'WORKTREE_DISK_INSUFFICIENT',
|
|
32
31
|
WORKTREE_LARGE_REPO_CONFIRM: 'WORKTREE_LARGE_REPO_CONFIRM',
|
|
33
32
|
WORKTREE_DIRTY: 'WORKTREE_DIRTY',
|
|
33
|
+
WORKTREE_DIRTY_CHOICE: 'WORKTREE_DIRTY_CHOICE',
|
|
34
|
+
WORKTREE_FILE_RECOVERY: 'WORKTREE_FILE_RECOVERY',
|
|
34
35
|
WORKTREE_UNMERGED: 'WORKTREE_UNMERGED',
|
|
35
36
|
WORKTREE_MISSING: 'WORKTREE_MISSING',
|
|
36
37
|
WORKTREE_UNSUPPORTED_REPO: 'WORKTREE_UNSUPPORTED_REPO',
|
|
@@ -56,6 +57,9 @@ const DEFAULT_ERROR_MESSAGES = Object.freeze({
|
|
|
56
57
|
WORKTREE_DISK_INSUFFICIENT: 'Not enough free disk space to create the worktree.',
|
|
57
58
|
WORKTREE_LARGE_REPO_CONFIRM: 'The repository is large; confirm before creating a worktree.',
|
|
58
59
|
WORKTREE_DIRTY: 'The worktree has uncommitted changes.',
|
|
60
|
+
WORKTREE_DIRTY_CHOICE:
|
|
61
|
+
'The project folder has uncommitted changes; choose whether to leave them there or carry them into the worktree.',
|
|
62
|
+
WORKTREE_FILE_RECOVERY: 'Retained file recovery records block worktree removal, including force. Inspect and explicitly delete them first.',
|
|
59
63
|
WORKTREE_UNMERGED: 'The worktree branch has commits that are not merged.',
|
|
60
64
|
WORKTREE_MISSING: 'The managed worktree no longer exists.',
|
|
61
65
|
WORKTREE_UNSUPPORTED_REPO: 'This repository does not support managed worktrees.',
|
|
@@ -302,29 +306,6 @@ export function normalizeWorktreeOwner(input) {
|
|
|
302
306
|
* @typedef {{ branch: string, kind: BranchOccupancyKind, sessionId?: string, path?: string }} BranchOccupancy
|
|
303
307
|
*/
|
|
304
308
|
|
|
305
|
-
/**
|
|
306
|
-
* @param {unknown} input
|
|
307
|
-
* @returns {BranchOccupancy}
|
|
308
|
-
*/
|
|
309
|
-
export function normalizeBranchOccupancy(input) {
|
|
310
|
-
const value = record(input, 'occupancy');
|
|
311
|
-
const branch = text(value.branch, 'occupancy.branch');
|
|
312
|
-
const kind = oneOf(value.kind, BRANCH_OCCUPANCY_KINDS, 'occupancy.kind');
|
|
313
|
-
if (kind === 'free') {
|
|
314
|
-
if (value.sessionId != null || value.path != null) {
|
|
315
|
-
throw new TypeError('A free branch carries no sessionId or path.');
|
|
316
|
-
}
|
|
317
|
-
return { branch, kind };
|
|
318
|
-
}
|
|
319
|
-
if (kind !== 'session' && value.sessionId != null) {
|
|
320
|
-
throw new TypeError('Only a session occupancy carries a sessionId.');
|
|
321
|
-
}
|
|
322
|
-
const occupancy = { branch, kind };
|
|
323
|
-
if (kind === 'session') occupancy.sessionId = text(value.sessionId, 'occupancy.sessionId');
|
|
324
|
-
if (value.path != null) occupancy.path = text(value.path, 'occupancy.path');
|
|
325
|
-
return occupancy;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
309
|
/**
|
|
329
310
|
* The branch menu's rows (the desktop DesktopGitBranch shape) for the local
|
|
330
311
|
* branch names the manager lists: `current` marks the branch checked out in
|
|
@@ -532,6 +513,35 @@ export function runRootReleasers(root, ctx) {
|
|
|
532
513
|
* `remove-worktree`: delete the retained worktree after dirty/unmerged checks.
|
|
533
514
|
*/
|
|
534
515
|
|
|
516
|
+
/**
|
|
517
|
+
* What happens to the project folder's uncommitted changes (tracked and
|
|
518
|
+
* untracked, ignored files excluded) when a session moves into a new
|
|
519
|
+
* worktree: `leave` keeps them in the project folder (the worktree starts at
|
|
520
|
+
* its HEAD); `carry` moves them into the worktree.
|
|
521
|
+
* @typedef {'leave'|'carry'} WorktreeCarryChoice
|
|
522
|
+
*/
|
|
523
|
+
export const WORKTREE_CARRY_CHOICES = Object.freeze(['leave', 'carry']);
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* requestWorktree input.
|
|
527
|
+
* @typedef {object} RequestWorktreeInput
|
|
528
|
+
* @property {string|null} [name]
|
|
529
|
+
* @property {number} [expectedRevision]
|
|
530
|
+
* @property {boolean} [confirmLarge] Acknowledges WORKTREE_LARGE_REPO_CONFIRM.
|
|
531
|
+
* @property {boolean} [provision] Create and enter the worktree now (the session moves) instead of at the next send.
|
|
532
|
+
* @property {WorktreeCarryChoice} [carryChanges] Answers WORKTREE_DIRTY_CHOICE (`details: { count, paths }`,
|
|
533
|
+
* `paths` capped to a preview), which a provisioning request without it raises while the project checkout has
|
|
534
|
+
* uncommitted changes. Scope is the whole repository checkout; ignored files (dependencies, build output, env
|
|
535
|
+
* files) are never carried and stay in the project folder.
|
|
536
|
+
*/
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* mergeWorkspace's `cleanup` after a successful merge: `removed: false` keeps
|
|
540
|
+
* the worktree and reports why (`code` of the refusal, e.g. WORKTREE_DIRTY,
|
|
541
|
+
* WORKTREE_UNMERGED, WORKTREE_REMOVAL_INCOMPLETE); the merge stands.
|
|
542
|
+
* @typedef {{ removed: boolean, code?: string, message?: string }} MergeCleanupResult
|
|
543
|
+
*/
|
|
544
|
+
|
|
535
545
|
/**
|
|
536
546
|
* @typedef {object} WorkspaceTransitionOptions
|
|
537
547
|
* @property {number} [expectedRevision] Reject with WORKTREE_REVISION_CONFLICT when the session moved on.
|