mixdog 1.4.1 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/scripts/build-shipped-history.mjs +115 -33
- package/scripts/gate-local.mjs +172 -61
- package/scripts/lib/gate-scheduler.mjs +138 -32
- package/scripts/lib/gate-timing-reporter.mjs +16 -0
- package/scripts/lib/run-node-tests.mjs +54 -29
- package/scripts/lib/scoped-temp.mjs +1 -0
- package/scripts/lib/test-failure-records.mjs +14 -1
- package/scripts/lib/test-failure-summary.mjs +16 -4
- package/scripts/lib/test-spawn-ledger.mjs +12 -3
- package/scripts/pptx-kit-index.mjs +5 -6
- package/scripts/suite-health.mjs +110 -28
- package/scripts/test.mjs +9 -14
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/explore/AGENT.md +22 -0
- package/src/agents/explore/agent.json +6 -0
- package/src/cli.mjs +7 -0
- package/src/defaults/agents.json +7 -0
- package/src/defaults/shipped-history.json +18 -2
- package/src/defaults/skills/browser-use/SKILL.md +4 -1
- package/src/defaults/skills/code-tidy/SKILL.md +71 -102
- package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
- package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
- package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
- package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
- package/src/defaults/skills/computer-use/SKILL.md +12 -2
- package/src/defaults/skills/goal-management/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +12 -6
- package/src/defaults/skills/setup/references/surfaces.md +2 -2
- package/src/defaults/skills/setup-guide/SKILL.md +5 -2
- package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
- package/src/defaults/skills/skill-creator/SKILL.md +85 -190
- package/src/defaults/skills/skill-creator/references/design.md +70 -162
- package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
- package/src/defaults/skills/test-suite/SKILL.md +194 -0
- package/src/defaults/skills/test-suite/references/case-management.md +51 -0
- package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
- package/src/defaults/skills/test-suite/references/reliability.md +49 -0
- package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
- package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
- package/src/defaults/skills/visualize/SKILL.md +6 -2
- package/src/lib/rules-builder.cjs +6 -30
- package/src/rules/lead/LEAD.md +3 -3
- package/src/rules/shared/20-research.md +3 -6
- package/src/rules/shared/40-editing.md +1 -0
- package/src/rules/shared/60-verification.md +10 -9
- package/src/rules/shared/70-delivery.md +4 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
- package/src/runtime/agent/orchestrator/config.mjs +12 -7
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
- package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
- package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
- package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
- package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
- package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
- package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/channels/lib/whisper-port.mjs +19 -10
- package/src/runtime/channels/lib/whisper-server.mjs +10 -1
- package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
- package/src/runtime/connectors/gmail-app-password.mjs +3 -3
- package/src/runtime/explore/explorer.mjs +93 -0
- package/src/runtime/explore/tool-defs.mjs +35 -0
- package/src/runtime/explore/tool.mjs +123 -0
- package/src/runtime/memory/index.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
- package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
- package/src/runtime/shared/agent-route-config.mjs +17 -2
- package/src/runtime/shared/atomic-file.mjs +103 -2
- package/src/runtime/shared/background-tasks.mjs +15 -6
- package/src/runtime/shared/connector-accounts.mjs +5 -5
- package/src/runtime/shared/connector-catalog.d.mts +11 -4
- package/src/runtime/shared/connector-catalog.mjs +60 -52
- package/src/runtime/shared/custom-provider-keys.mjs +20 -0
- package/src/runtime/shared/file-has-data.mjs +26 -2
- package/src/runtime/shared/file-lock.mjs +13 -15
- package/src/runtime/shared/git-launch.d.mts +9 -0
- package/src/runtime/shared/git-launch.mjs +213 -0
- package/src/runtime/shared/git-runner-worker.mjs +2 -1
- package/src/runtime/shared/llm/cost.mjs +6 -1
- package/src/runtime/shared/llm/http-agent.mjs +313 -29
- package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
- package/src/runtime/shared/llm/model-catalog.mjs +46 -6
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
- package/src/runtime/shared/llm/system-proxy.mjs +452 -0
- package/src/runtime/shared/provider-accounts.mjs +52 -2
- package/src/runtime/shared/staged-install-worker.mjs +5 -0
- package/src/runtime/shared/staged-update.mjs +47 -2
- package/src/runtime/shared/tool-surface.mjs +110 -36
- package/src/runtime/shared/tool-work-units.mjs +4 -0
- package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
- package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
- package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
- package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
- package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
- package/src/runtime/shared/worktrees/git.mjs +57 -0
- package/src/runtime/shared/worktrees/manager.mjs +205 -137
- package/src/runtime/tidy/install.mjs +95 -15
- package/src/runtime/tidy/languages.mjs +17 -15
- package/src/runtime/tidy/process.mjs +7 -4
- package/src/runtime/tidy/resolve.mjs +19 -16
- package/src/runtime/tidy/runners/biome.mjs +3 -3
- package/src/runtime/tidy/tool.mjs +1 -1
- package/src/runtime/tidy/worktree.mjs +10 -6
- package/src/runtime/visualize-bridge/client.mjs +4 -1
- package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
- package/src/runtime/web-search/index.mjs +1 -1
- package/src/session-runtime/agent-task-sessions.mjs +410 -353
- package/src/session-runtime/boot/tools.mjs +2 -0
- package/src/session-runtime/bot-status.mjs +103 -103
- package/src/session-runtime/connector-merge-migration.mjs +18 -15
- package/src/session-runtime/conversation-import/discovery.mjs +159 -40
- package/src/session-runtime/conversation-import/service.mjs +9 -3
- package/src/session-runtime/handoff-attachments.mjs +1 -1
- package/src/session-runtime/held-requests.mjs +3 -1
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
- package/src/session-runtime/lifecycle/teardown.mjs +1 -1
- package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
- package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
- package/src/session-runtime/mixy-heartbeat.mjs +1 -1
- package/src/session-runtime/provider-auth/login.mjs +2 -2
- package/src/session-runtime/resource-mcp-api.mjs +98 -35
- package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
- package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
- package/src/session-runtime/resource-mcp-servers.mjs +5 -4
- package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
- package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
- package/src/session-runtime/services/daemon-candidate.mjs +4 -1
- package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
- package/src/session-runtime/services/plugin-admin.mjs +10 -6
- package/src/session-runtime/services/provider-admin.mjs +281 -10
- package/src/session-runtime/session-turn-api.mjs +2 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
- package/src/session-runtime/settings-system-api.mjs +1 -1
- package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
- package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
- package/src/session-runtime/setup-tool/executor.mjs +6 -2
- package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
- package/src/session-runtime/turn/agent-ops.mjs +23 -0
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
- package/src/session-runtime/workspace/activity-guards.mjs +176 -0
- package/src/session-runtime/workspace/session-view.mjs +149 -0
- package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
- package/src/session-runtime/workspace/transition-runner.mjs +188 -0
- package/src/session-runtime/workspace/transitions.mjs +108 -1066
- package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
- package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
- package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
- package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
- package/src/standalone/daemon-desktop-runtime.mjs +2 -0
- package/src/standalone/daemon-session-control.mjs +151 -136
- package/src/standalone/daemon.mjs +7 -0
- package/src/standalone/desktop-service-registry.mjs +9 -0
- package/src/standalone/local-session-runtime.mjs +5 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
- package/src/standalone/session-client.mjs +5 -1
- package/src/standalone/session-frame-stream.mjs +83 -11
- package/src/standalone/session-protocol.mjs +4 -0
- package/src/standalone/session-service/agent-tree.mjs +45 -9
- package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
- package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
- package/src/standalone/session-service/stored-reader.mjs +127 -40
- package/src/tui/App.jsx +154 -164
- package/src/tui/app/app-view/floating-panel.jsx +3 -0
- package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
- package/src/tui/app/clipboard.mjs +21 -10
- package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
- package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
- package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
- package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
- package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
- package/src/tui/app/provider-setup-picker.mjs +20 -13
- package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
- package/src/tui/app/slash-dispatch.mjs +1 -1
- package/src/tui/app/use-btw-panel.mjs +29 -0
- package/src/tui/app/use-context-panel-refresh.mjs +31 -0
- package/src/tui/app/use-model-catalog-cache.mjs +13 -10
- package/src/tui/app/use-mouse-input.mjs +11 -0
- package/src/tui/app/use-onboarding-start.mjs +8 -3
- package/src/tui/app/use-panel-transition.mjs +10 -1
- package/src/tui/app/use-pasted-buffers.mjs +28 -20
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
- package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
- package/src/tui/app/use-prompt-queue-history.mjs +3 -2
- package/src/tui/app/use-selection-focus.mjs +11 -2
- package/src/tui/app/use-selection-geometry.mjs +18 -9
- package/src/tui/app/use-selection-paint.mjs +4 -4
- package/src/tui/app/use-selection-stitch.mjs +3 -3
- package/src/tui/app/use-slash-palette.mjs +62 -0
- package/src/tui/app/use-terminal-chrome.mjs +2 -2
- package/src/tui/app/use-transcript-activity.mjs +47 -32
- package/src/tui/app/use-transcript-follow.mjs +12 -4
- package/src/tui/app/use-transcript-scroll.mjs +46 -17
- package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
- package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
- package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
- package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +21 -11
- package/src/tui/app/use-tui-ready.mjs +18 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
- package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
- package/src/tui/components/Picker.jsx +27 -3
- package/src/tui/components/SignInUrl.jsx +68 -0
- package/src/tui/components/TextEntryPanel.jsx +76 -46
- package/src/tui/components/sign-in-url-layout.mjs +24 -0
- package/src/tui/dist/index.mjs +1896 -1341
- package/src/tui/session/goal-turn-state.mjs +6 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/extensions.mjs +8 -0
- package/src/tui/session/session-api/intake/abort.mjs +2 -1
- package/src/tui/session/session-api/intake.mjs +2 -0
- package/src/tui/session/session-api/integrations.mjs +2 -2
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/session-api/settings.mjs +2 -2
- package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
- package/src/tui/session/transcript-spill.mjs +92 -4
- package/src/tui/session/transcript-store.mjs +2 -0
- package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from './file-recovery.mjs';
|
|
41
41
|
import { lstat, mkdir, readdir, readFile, rm, rmdir, stat } from 'node:fs/promises';
|
|
42
42
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
43
|
+
import { isPidAlive } from '../pid-liveness.mjs';
|
|
43
44
|
import {
|
|
44
45
|
managedWorktreePath,
|
|
45
46
|
managedWorktreeRoot,
|
|
@@ -86,7 +87,7 @@ import {
|
|
|
86
87
|
} from './include.mjs';
|
|
87
88
|
import { linkWorktreeDependencies } from './dependencies.mjs';
|
|
88
89
|
import { withRepoLock as withRepoMutationLock } from './lock.mjs';
|
|
89
|
-
import {
|
|
90
|
+
import { holdWorkspaceActivityAsync } from './activity-gate.mjs';
|
|
90
91
|
import { withAdvisoryLocks } from '../../agent/orchestrator/tools/builtin/advisory-lock.mjs';
|
|
91
92
|
import { mergeBranchIntoCheckout, operationInProgress } from './merge.mjs';
|
|
92
93
|
import { readOwnerLease, removeOwnerLease, sameOwner, writeOwnerLease } from './owner.mjs';
|
|
@@ -124,17 +125,7 @@ function sessionRootOf(record) {
|
|
|
124
125
|
// ---------------------------------------------------------------------------
|
|
125
126
|
// Host configuration (settings source, free-space probe); tests override it.
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
function processAlive(pid) {
|
|
129
|
-
try {
|
|
130
|
-
process.kill(pid, 0);
|
|
131
|
-
return true;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
return error?.code === 'EPERM';
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const DEFAULT_CONFIG = Object.freeze({ settings: null, freeBytes: freeBytesAt, processAlive });
|
|
128
|
+
const DEFAULT_CONFIG = Object.freeze({ settings: null, freeBytes: freeBytesAt, processAlive: isPidAlive });
|
|
138
129
|
let config = { ...DEFAULT_CONFIG };
|
|
139
130
|
|
|
140
131
|
/**
|
|
@@ -979,6 +970,174 @@ async function snapshotHolderLive(record, live) {
|
|
|
979
970
|
return !!lease && lease.pid !== process.pid && leaseProcessAlive(lease);
|
|
980
971
|
}
|
|
981
972
|
|
|
973
|
+
/** The project folder's uncommitted changes under the requested `carryChanges` mode; `ask` refuses while there are any. */
|
|
974
|
+
async function judgePendingChanges(repo, projectSubdir, carryChanges) {
|
|
975
|
+
// A carry is judged by the same status read that counts the changes.
|
|
976
|
+
let pendingChanges = [];
|
|
977
|
+
if (carryChanges === 'carry') pendingChanges = await assertCarryable(repo.topLevel, projectSubdir);
|
|
978
|
+
else if (carryChanges) pendingChanges = await uncommittedChanges(repo.topLevel, projectSubdir);
|
|
979
|
+
if (pendingChanges.length && carryChanges === 'ask') {
|
|
980
|
+
throw worktreeError('WORKTREE_DIRTY_CHOICE', undefined, {
|
|
981
|
+
count: pendingChanges.length,
|
|
982
|
+
paths: pendingChanges.slice(0, DIRTY_CHOICE_PREVIEW),
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return pendingChanges;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** The commit a new worktree starts from and the branch a merge targets by default. */
|
|
989
|
+
async function resolveStartPoint(repo, { baseBranch, startCommit, signal }) {
|
|
990
|
+
let baseCommit = repo.headCommit;
|
|
991
|
+
let mergeTarget = repo.currentBranch;
|
|
992
|
+
if (baseBranch) {
|
|
993
|
+
baseCommit = await branchCommit(repo.projectPath, baseBranch, { signal });
|
|
994
|
+
if (!baseCommit) {
|
|
995
|
+
throw worktreeError('WORKTREE_UNSUPPORTED_REPO', `Base branch ${baseBranch} does not exist.`, { baseBranch });
|
|
996
|
+
}
|
|
997
|
+
mergeTarget = baseBranch;
|
|
998
|
+
}
|
|
999
|
+
// A new branch at an explicit start point (`git worktree add -b <b> <path> <commit-ish>`).
|
|
1000
|
+
if (startCommit) baseCommit = await resolveCommit(repo.projectPath, startCommit, signal);
|
|
1001
|
+
return { baseCommit, mergeTarget };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** The record a creation reserves before git is asked to add the checkout. */
|
|
1005
|
+
function newCreationRecord({
|
|
1006
|
+
slug,
|
|
1007
|
+
branch,
|
|
1008
|
+
repo,
|
|
1009
|
+
owner,
|
|
1010
|
+
txId,
|
|
1011
|
+
baseCommit,
|
|
1012
|
+
mergeTarget,
|
|
1013
|
+
existingBranch,
|
|
1014
|
+
exactBranch,
|
|
1015
|
+
tracked,
|
|
1016
|
+
includePlan,
|
|
1017
|
+
}) {
|
|
1018
|
+
return {
|
|
1019
|
+
id: slug,
|
|
1020
|
+
txId,
|
|
1021
|
+
repoCommonDir: repo.repoCommonDir,
|
|
1022
|
+
projectPath: repo.projectPath,
|
|
1023
|
+
projectSubdir: projectSubdirOf(repo.topLevel, repo.projectPath),
|
|
1024
|
+
path: managedWorktreePath(repo.repoCommonDir, slug),
|
|
1025
|
+
branch,
|
|
1026
|
+
baseCommit,
|
|
1027
|
+
// An existing branch has no base it branched from: it merges where a
|
|
1028
|
+
// merge first takes it (recorded then), and until that its own work is
|
|
1029
|
+
// what it gained here since baseCommit.
|
|
1030
|
+
mergeTarget: existingBranch ? null : mergeTarget,
|
|
1031
|
+
createdAt: Date.now(),
|
|
1032
|
+
ownerSessionId: owner.sessionId,
|
|
1033
|
+
existingBranch,
|
|
1034
|
+
exactBranch,
|
|
1035
|
+
state: 'creating',
|
|
1036
|
+
disposable: false,
|
|
1037
|
+
warnings: [...(tracked.submodules ? ['submodules'] : []), ...includePlan.warnings],
|
|
1038
|
+
resources: { worktree: null, lock: null, owner: null, includes: [] },
|
|
1039
|
+
deleting: null,
|
|
1040
|
+
recovery: null,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Locks the fresh checkout, binds its owner lease, moves or leaves the
|
|
1046
|
+
* project folder's changes, applies the includes and links dependencies.
|
|
1047
|
+
* `carry.moved` is set as soon as changes were carried, so a later failure
|
|
1048
|
+
* can put them back.
|
|
1049
|
+
*/
|
|
1050
|
+
async function populateCheckout({
|
|
1051
|
+
record,
|
|
1052
|
+
owner,
|
|
1053
|
+
repo,
|
|
1054
|
+
projectSubdir,
|
|
1055
|
+
signal,
|
|
1056
|
+
pendingChanges,
|
|
1057
|
+
carryChanges,
|
|
1058
|
+
baseCommit,
|
|
1059
|
+
includePlan,
|
|
1060
|
+
carry,
|
|
1061
|
+
}) {
|
|
1062
|
+
await setResource(record, 'worktree', 'created');
|
|
1063
|
+
throwIfAborted(signal);
|
|
1064
|
+
await setResource(record, 'lock', 'pending');
|
|
1065
|
+
await runGit(repo.projectPath, ['worktree', 'lock', '--reason', `mixdog session=${owner.sessionId}`, record.path]);
|
|
1066
|
+
await setResource(record, 'lock', 'created');
|
|
1067
|
+
await setResource(record, 'owner', 'pending');
|
|
1068
|
+
await writeOwnerLease(record, owner);
|
|
1069
|
+
await setResource(record, 'owner', 'created');
|
|
1070
|
+
// Carried before the include entries are copied, so an include never
|
|
1071
|
+
// stands in the way of a carried file; the status is read again here,
|
|
1072
|
+
// right before acting.
|
|
1073
|
+
let changes = pendingChanges.length ? { mode: 'left', count: pendingChanges.length } : null;
|
|
1074
|
+
if (carryChanges === 'carry') {
|
|
1075
|
+
carry.moved = await carryUncommittedChanges({
|
|
1076
|
+
topLevel: repo.topLevel,
|
|
1077
|
+
projectSubdir,
|
|
1078
|
+
worktreePath: record.path,
|
|
1079
|
+
baseCommit,
|
|
1080
|
+
label: record.id,
|
|
1081
|
+
});
|
|
1082
|
+
// The recovery ref stays until the caller has committed the move
|
|
1083
|
+
// (dropCarryRecoveryRef) or put the changes back (restoreCarriedChanges).
|
|
1084
|
+
changes = carry.moved.count
|
|
1085
|
+
? { mode: 'carried', count: carry.moved.count, recoveryRef: carry.moved.recoveryRef }
|
|
1086
|
+
: null;
|
|
1087
|
+
}
|
|
1088
|
+
const applied = await applyIncludes(includePlan, record.path, (ledger) => recordInclude(record, ledger));
|
|
1089
|
+
throwIfAborted(signal);
|
|
1090
|
+
// After the includes: one that already brought a node_modules wins.
|
|
1091
|
+
const linkedDependencies = await linkWorktreeDependencies({
|
|
1092
|
+
mainDir: repo.projectPath,
|
|
1093
|
+
worktreeDir: projectSubdir ? join(record.path, ...projectSubdir.split('/')) : record.path,
|
|
1094
|
+
worktreeRel: projectSubdir,
|
|
1095
|
+
signal,
|
|
1096
|
+
record: (ledger) => recordInclude(record, ledger),
|
|
1097
|
+
});
|
|
1098
|
+
throwIfAborted(signal);
|
|
1099
|
+
record.warnings = [...record.warnings, ...applied.warnings];
|
|
1100
|
+
const made = (record.resources.includes || []).filter((item) => item.status === 'created');
|
|
1101
|
+
const rels = (predicate) => made.filter(predicate).map((item) => item.rel);
|
|
1102
|
+
// What creation did, for the agent's note at the first turn there (transitions.mjs).
|
|
1103
|
+
const prepared = {
|
|
1104
|
+
root: repo.topLevel,
|
|
1105
|
+
copied: rels((item) => item.mode === 'copy'),
|
|
1106
|
+
linked: rels((item) => item.mode === 'link' && !item.dependency),
|
|
1107
|
+
dependencies: linkedDependencies,
|
|
1108
|
+
skipped: [...includePlan.warnings, ...applied.warnings],
|
|
1109
|
+
};
|
|
1110
|
+
// Whichever path made it (a worktree task, a branch selection, `git
|
|
1111
|
+
// worktree add`), the first turn a session runs here prepares its
|
|
1112
|
+
// dependencies (claimWorktreeSetup).
|
|
1113
|
+
record.setup = { pending: true, prepared };
|
|
1114
|
+
record.state = 'ready';
|
|
1115
|
+
await putRecord(record);
|
|
1116
|
+
return { ...entryView(record), prepared, ...(changes ? { changes } : {}) };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** The error a failed creation throws, after its carried changes and partial checkout are undone. */
|
|
1120
|
+
async function failCreation(error, { record, carried, repo, projectSubdir, needed, root }) {
|
|
1121
|
+
// Carried already: the changes go back to the project folder before the
|
|
1122
|
+
// worktree that holds them is rolled back (the recovery ref keeps them
|
|
1123
|
+
// either way).
|
|
1124
|
+
let carryRestore = null;
|
|
1125
|
+
if (carried?.recoveryRef) {
|
|
1126
|
+
carryRestore = await restoreFromRecoveryRef(repo.topLevel, projectSubdir, carried.recoveryRef)
|
|
1127
|
+
.then(() => ({ restored: true, recoveryRef: carried.recoveryRef }))
|
|
1128
|
+
.catch((restoreError) => ({
|
|
1129
|
+
restored: false,
|
|
1130
|
+
recoveryRef: carried.recoveryRef,
|
|
1131
|
+
error: String(restoreError?.message || restoreError),
|
|
1132
|
+
}));
|
|
1133
|
+
}
|
|
1134
|
+
const rollback = await rollbackCreation(record, error);
|
|
1135
|
+
const failure = isDiskFullError(error) ? await diskFullFailure(error, { needed, root }) : error;
|
|
1136
|
+
if (!rollback.complete) failure.rollback = rollback;
|
|
1137
|
+
if (carryRestore) failure.carryRestore = carryRestore;
|
|
1138
|
+
return failure;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
982
1141
|
/**
|
|
983
1142
|
* `carryChanges` decides the project folder's uncommitted changes (tracked
|
|
984
1143
|
* and untracked, ignored excluded), judged under the repository lock:
|
|
@@ -1034,29 +1193,10 @@ export async function createManagedWorktree({
|
|
|
1034
1193
|
throwIfAborted(signal);
|
|
1035
1194
|
|
|
1036
1195
|
const projectSubdir = projectSubdirOf(repo.topLevel, repo.projectPath);
|
|
1037
|
-
|
|
1038
|
-
let pendingChanges = [];
|
|
1039
|
-
if (carryChanges === 'carry') pendingChanges = await assertCarryable(repo.topLevel, projectSubdir);
|
|
1040
|
-
else if (carryChanges) pendingChanges = await uncommittedChanges(repo.topLevel, projectSubdir);
|
|
1041
|
-
if (pendingChanges.length && carryChanges === 'ask') {
|
|
1042
|
-
throw worktreeError('WORKTREE_DIRTY_CHOICE', undefined, {
|
|
1043
|
-
count: pendingChanges.length,
|
|
1044
|
-
paths: pendingChanges.slice(0, DIRTY_CHOICE_PREVIEW),
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1196
|
+
const pendingChanges = await judgePendingChanges(repo, projectSubdir, carryChanges);
|
|
1047
1197
|
|
|
1048
1198
|
const settings = await currentWorktreeSettings();
|
|
1049
|
-
|
|
1050
|
-
let mergeTarget = repo.currentBranch;
|
|
1051
|
-
if (baseBranch) {
|
|
1052
|
-
baseCommit = await branchCommit(repo.projectPath, baseBranch, { signal });
|
|
1053
|
-
if (!baseCommit) {
|
|
1054
|
-
throw worktreeError('WORKTREE_UNSUPPORTED_REPO', `Base branch ${baseBranch} does not exist.`, { baseBranch });
|
|
1055
|
-
}
|
|
1056
|
-
mergeTarget = baseBranch;
|
|
1057
|
-
}
|
|
1058
|
-
// A new branch at an explicit start point (`git worktree add -b <b> <path> <commit-ish>`).
|
|
1059
|
-
if (startCommit) baseCommit = await resolveCommit(repo.projectPath, startCommit, signal);
|
|
1199
|
+
const { baseCommit, mergeTarget } = await resolveStartPoint(repo, { baseBranch, startCommit, signal });
|
|
1060
1200
|
|
|
1061
1201
|
// The main checkout's local env files come along on their own;
|
|
1062
1202
|
// `.worktreeinclude` applies on top.
|
|
@@ -1099,7 +1239,7 @@ export async function createManagedWorktree({
|
|
|
1099
1239
|
}
|
|
1100
1240
|
);
|
|
1101
1241
|
let record = await reserve();
|
|
1102
|
-
|
|
1242
|
+
const carry = { moved: null };
|
|
1103
1243
|
|
|
1104
1244
|
try {
|
|
1105
1245
|
await mkdir(root, { recursive: true });
|
|
@@ -1120,84 +1260,20 @@ export async function createManagedWorktree({
|
|
|
1120
1260
|
await setResource(record, 'worktree', 'pending');
|
|
1121
1261
|
await addCheckout(record);
|
|
1122
1262
|
}
|
|
1123
|
-
await
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
'lock',
|
|
1129
|
-
'--reason',
|
|
1130
|
-
`mixdog session=${owner.sessionId}`,
|
|
1131
|
-
record.path,
|
|
1132
|
-
]);
|
|
1133
|
-
await setResource(record, 'lock', 'created');
|
|
1134
|
-
await setResource(record, 'owner', 'pending');
|
|
1135
|
-
await writeOwnerLease(record, owner);
|
|
1136
|
-
await setResource(record, 'owner', 'created');
|
|
1137
|
-
// Carried before the include entries are copied, so an include never
|
|
1138
|
-
// stands in the way of a carried file; the status is read again here,
|
|
1139
|
-
// right before acting.
|
|
1140
|
-
let changes = pendingChanges.length ? { mode: 'left', count: pendingChanges.length } : null;
|
|
1141
|
-
if (carryChanges === 'carry') {
|
|
1142
|
-
carried = await carryUncommittedChanges({
|
|
1143
|
-
topLevel: repo.topLevel,
|
|
1144
|
-
projectSubdir,
|
|
1145
|
-
worktreePath: record.path,
|
|
1146
|
-
baseCommit,
|
|
1147
|
-
label: record.id,
|
|
1148
|
-
});
|
|
1149
|
-
// The recovery ref stays until the caller has committed the move
|
|
1150
|
-
// (dropCarryRecoveryRef) or put the changes back (restoreCarriedChanges).
|
|
1151
|
-
changes = carried.count ? { mode: 'carried', count: carried.count, recoveryRef: carried.recoveryRef } : null;
|
|
1152
|
-
}
|
|
1153
|
-
const applied = await applyIncludes(includePlan, record.path, (ledger) => recordInclude(record, ledger));
|
|
1154
|
-
throwIfAborted(signal);
|
|
1155
|
-
// After the includes: one that already brought a node_modules wins.
|
|
1156
|
-
const linkedDependencies = await linkWorktreeDependencies({
|
|
1157
|
-
mainDir: repo.projectPath,
|
|
1158
|
-
worktreeDir: projectSubdir ? join(record.path, ...projectSubdir.split('/')) : record.path,
|
|
1159
|
-
worktreeRel: projectSubdir,
|
|
1263
|
+
return await populateCheckout({
|
|
1264
|
+
record,
|
|
1265
|
+
owner,
|
|
1266
|
+
repo,
|
|
1267
|
+
projectSubdir,
|
|
1160
1268
|
signal,
|
|
1161
|
-
|
|
1269
|
+
pendingChanges,
|
|
1270
|
+
carryChanges,
|
|
1271
|
+
baseCommit,
|
|
1272
|
+
includePlan,
|
|
1273
|
+
carry,
|
|
1162
1274
|
});
|
|
1163
|
-
throwIfAborted(signal);
|
|
1164
|
-
record.warnings = [...record.warnings, ...applied.warnings];
|
|
1165
|
-
const made = (record.resources.includes || []).filter((item) => item.status === 'created');
|
|
1166
|
-
const rels = (predicate) => made.filter(predicate).map((item) => item.rel);
|
|
1167
|
-
// What creation did, for the agent's note at the first turn there (transitions.mjs).
|
|
1168
|
-
const prepared = {
|
|
1169
|
-
root: repo.topLevel,
|
|
1170
|
-
copied: rels((item) => item.mode === 'copy'),
|
|
1171
|
-
linked: rels((item) => item.mode === 'link' && !item.dependency),
|
|
1172
|
-
dependencies: linkedDependencies,
|
|
1173
|
-
skipped: [...includePlan.warnings, ...applied.warnings],
|
|
1174
|
-
};
|
|
1175
|
-
// Whichever path made it (a worktree task, a branch selection, `git
|
|
1176
|
-
// worktree add`), the first turn a session runs here prepares its
|
|
1177
|
-
// dependencies (claimWorktreeSetup).
|
|
1178
|
-
record.setup = { pending: true, prepared };
|
|
1179
|
-
record.state = 'ready';
|
|
1180
|
-
await putRecord(record);
|
|
1181
|
-
return { ...entryView(record), prepared, ...(changes ? { changes } : {}) };
|
|
1182
1275
|
} catch (error) {
|
|
1183
|
-
|
|
1184
|
-
// worktree that holds them is rolled back (the recovery ref keeps them
|
|
1185
|
-
// either way).
|
|
1186
|
-
let carryRestore = null;
|
|
1187
|
-
if (carried?.recoveryRef) {
|
|
1188
|
-
carryRestore = await restoreFromRecoveryRef(repo.topLevel, projectSubdir, carried.recoveryRef)
|
|
1189
|
-
.then(() => ({ restored: true, recoveryRef: carried.recoveryRef }))
|
|
1190
|
-
.catch((restoreError) => ({
|
|
1191
|
-
restored: false,
|
|
1192
|
-
recoveryRef: carried.recoveryRef,
|
|
1193
|
-
error: String(restoreError?.message || restoreError),
|
|
1194
|
-
}));
|
|
1195
|
-
}
|
|
1196
|
-
const rollback = await rollbackCreation(record, error);
|
|
1197
|
-
const failure = isDiskFullError(error) ? await diskFullFailure(error, { needed, root }) : error;
|
|
1198
|
-
if (!rollback.complete) failure.rollback = rollback;
|
|
1199
|
-
if (carryRestore) failure.carryRestore = carryRestore;
|
|
1200
|
-
throw failure;
|
|
1276
|
+
throw await failCreation(error, { record, carried: carry.moved, repo, projectSubdir, needed, root });
|
|
1201
1277
|
}
|
|
1202
1278
|
|
|
1203
1279
|
async function reserveName() {
|
|
@@ -1213,30 +1289,19 @@ export async function createManagedWorktree({
|
|
|
1213
1289
|
if (!(await validBranchName(repo.projectPath, branch))) {
|
|
1214
1290
|
throw worktreeError('WORKTREE_UNSUPPORTED_REPO', `Branch name ${branch} is not valid.`, { branch });
|
|
1215
1291
|
}
|
|
1216
|
-
const candidate = {
|
|
1217
|
-
|
|
1218
|
-
txId,
|
|
1219
|
-
repoCommonDir: repo.repoCommonDir,
|
|
1220
|
-
projectPath: repo.projectPath,
|
|
1221
|
-
projectSubdir: projectSubdirOf(repo.topLevel, repo.projectPath),
|
|
1222
|
-
path: managedWorktreePath(repo.repoCommonDir, slug),
|
|
1292
|
+
const candidate = newCreationRecord({
|
|
1293
|
+
slug,
|
|
1223
1294
|
branch,
|
|
1295
|
+
repo,
|
|
1296
|
+
owner,
|
|
1297
|
+
txId,
|
|
1224
1298
|
baseCommit,
|
|
1225
|
-
|
|
1226
|
-
// merge first takes it (recorded then), and until that its own work is
|
|
1227
|
-
// what it gained here since baseCommit.
|
|
1228
|
-
mergeTarget: existingBranch ? null : mergeTarget,
|
|
1229
|
-
createdAt: Date.now(),
|
|
1230
|
-
ownerSessionId: owner.sessionId,
|
|
1299
|
+
mergeTarget,
|
|
1231
1300
|
existingBranch,
|
|
1232
1301
|
exactBranch: requestedBranch !== null,
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
resources: { worktree: null, lock: null, owner: null, includes: [] },
|
|
1237
|
-
deleting: null,
|
|
1238
|
-
recovery: null,
|
|
1239
|
-
};
|
|
1302
|
+
tracked,
|
|
1303
|
+
includePlan,
|
|
1304
|
+
});
|
|
1240
1305
|
return (await insertRecord(candidate)) ? candidate : null;
|
|
1241
1306
|
}
|
|
1242
1307
|
});
|
|
@@ -1362,9 +1427,12 @@ export async function createSnapshotWorktree({
|
|
|
1362
1427
|
* one is admitted there until the returned release. A folder no longer a
|
|
1363
1428
|
* checkout admits no consumer (activity-gate tracks git checkouts only).
|
|
1364
1429
|
*/
|
|
1365
|
-
function holdSnapshotFolder(record) {
|
|
1430
|
+
async function holdSnapshotFolder(record) {
|
|
1366
1431
|
if (!existsSync(join(record.path, '.git'))) return () => {};
|
|
1367
|
-
|
|
1432
|
+
const release = await holdWorkspaceActivityAsync([record.path]);
|
|
1433
|
+
// Settles once the removal was attempted. It fails only when workspace
|
|
1434
|
+
// admission stopped (reported there), which keeps the folder held.
|
|
1435
|
+
return () => release().catch(() => {});
|
|
1368
1436
|
}
|
|
1369
1437
|
|
|
1370
1438
|
/** A refusal that only says "something still runs there" (retry once it ends). */
|
|
@@ -1871,11 +1939,11 @@ async function removeLocked(current, { force, sessionId, preserveBranch = false,
|
|
|
1871
1939
|
}
|
|
1872
1940
|
// Every deletion attempt of a snapshot, a resumed one included, waits
|
|
1873
1941
|
// for (refuses on) its consumers.
|
|
1874
|
-
const release = current.kind === WORKTREE_SNAPSHOT_KIND ? holdSnapshotFolder(current) : () => {};
|
|
1942
|
+
const release = current.kind === WORKTREE_SNAPSHOT_KIND ? await holdSnapshotFolder(current) : () => {};
|
|
1875
1943
|
try {
|
|
1876
1944
|
return await decideAndFinishDeletion(current, { force, preserveBranch, safeOnly });
|
|
1877
1945
|
} finally {
|
|
1878
|
-
release();
|
|
1946
|
+
await release();
|
|
1879
1947
|
}
|
|
1880
1948
|
}
|
|
1881
1949
|
|
|
@@ -2044,7 +2112,7 @@ export async function reconcileManagedWorktrees({
|
|
|
2044
2112
|
// surviving folder since still keeps it.
|
|
2045
2113
|
let release;
|
|
2046
2114
|
try {
|
|
2047
|
-
release = holdSnapshotFolder(record);
|
|
2115
|
+
release = await holdSnapshotFolder(record);
|
|
2048
2116
|
} catch (error) {
|
|
2049
2117
|
if (!snapshotInUse(error)) throw error;
|
|
2050
2118
|
report.kept.push({ id: record.id, reason: 'snapshot-in-use' });
|
|
@@ -2053,7 +2121,7 @@ export async function reconcileManagedWorktrees({
|
|
|
2053
2121
|
try {
|
|
2054
2122
|
reportDeletion(record, await finishDeletion(record));
|
|
2055
2123
|
} finally {
|
|
2056
|
-
release();
|
|
2124
|
+
await release();
|
|
2057
2125
|
}
|
|
2058
2126
|
continue;
|
|
2059
2127
|
}
|
|
@@ -2069,7 +2137,7 @@ export async function reconcileManagedWorktrees({
|
|
|
2069
2137
|
let release = null;
|
|
2070
2138
|
if (!(await snapshotHolderLive(record, live))) {
|
|
2071
2139
|
try {
|
|
2072
|
-
release = holdSnapshotFolder(record);
|
|
2140
|
+
release = await holdSnapshotFolder(record);
|
|
2073
2141
|
} catch (error) {
|
|
2074
2142
|
if (!snapshotInUse(error)) throw error;
|
|
2075
2143
|
}
|
|
@@ -2085,7 +2153,7 @@ export async function reconcileManagedWorktrees({
|
|
|
2085
2153
|
await putRecord(record);
|
|
2086
2154
|
reportDeletion(record, await finishDeletion(record));
|
|
2087
2155
|
} finally {
|
|
2088
|
-
release();
|
|
2156
|
+
await release();
|
|
2089
2157
|
}
|
|
2090
2158
|
continue;
|
|
2091
2159
|
}
|
|
@@ -4,12 +4,22 @@
|
|
|
4
4
|
// Policy gating happens BEFORE any network call: 'never' refuses, 'ask' returns
|
|
5
5
|
// needsApproval unless the call carried approveDownloads:true, 'auto' proceeds.
|
|
6
6
|
// Toolchain engines (rustfmt, gofmt, zig fmt, ...) are never downloadable.
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
chmodSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
statSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from 'node:fs';
|
|
8
18
|
import { fileURLToPath } from 'node:url';
|
|
9
19
|
import { join } from 'node:path';
|
|
10
20
|
import { MAX_NATIVE_BINARY_DOWNLOAD_BYTES, streamResponseToFile } from '../shared/bounded-download.mjs';
|
|
11
|
-
import { platformEntryKey, platformKey, sha256File } from '../shared/native-asset.mjs';
|
|
12
|
-
import {
|
|
21
|
+
import { platformEntryKey, platformKey, sha256File, validSha256 } from '../shared/native-asset.mjs';
|
|
22
|
+
import { fileHasDataAsync } from '../shared/file-has-data.mjs';
|
|
13
23
|
import { readJsonSafe } from '../shared/json-file.mjs';
|
|
14
24
|
import { ENGINE_CATALOG } from './engines.mjs';
|
|
15
25
|
import { extractArchive } from './extract.mjs';
|
|
@@ -45,14 +55,73 @@ export function manifestAsset(manifest, id, pkey = platformEntryKey(manifest?.en
|
|
|
45
55
|
return { entry, asset, version: String(entry.version || '0'), pkey: entry?.assets?.[pkey] ? pkey : 'any' };
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
// The installer records the extracted binary's sha256 beside it; a reuse
|
|
59
|
+
// re-hashes the binary against that record. Like the other managed binaries
|
|
60
|
+
// (mixdog-spawn / patch / graph resolve their path once per process), the hash
|
|
61
|
+
// runs once per process per binary, again only when its size or mtime changes.
|
|
62
|
+
// An install from before the record existed has no digest file: its first use
|
|
63
|
+
// records the digest (trust on first use). An existing record that is invalid
|
|
64
|
+
// or does not match is never overwritten; that binary is tampered or corrupt.
|
|
65
|
+
export const ENGINE_DIGEST_FILE = '.engine.sha256';
|
|
66
|
+
const verifiedEngineBins = new Map();
|
|
67
|
+
|
|
68
|
+
function recordEngineDigest(digestFile, digest) {
|
|
69
|
+
const tmp = `${digestFile}.tmp-${process.pid}-${Date.now()}`;
|
|
70
|
+
try {
|
|
71
|
+
writeFileSync(tmp, digest);
|
|
72
|
+
renameSync(tmp, digestFile);
|
|
73
|
+
} catch {
|
|
74
|
+
/* unrecorded: this install is trusted for this process only */
|
|
75
|
+
} finally {
|
|
76
|
+
try {
|
|
77
|
+
rmSync(tmp, { force: true });
|
|
78
|
+
} catch {
|
|
79
|
+
/* a leftover temp file never matches the digest path */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The recorded digest decides whether a binary is intact. Only an install from
|
|
85
|
+
// before the record existed has none; there alone the content probe keeps a
|
|
86
|
+
// crash-torn (empty or NUL-filled) binary from becoming the digest it is
|
|
87
|
+
// trusted by. A binary that cannot be statted, read or hashed cannot be
|
|
88
|
+
// verified, so it is not reused.
|
|
89
|
+
async function engineBinaryIntact(dir, full) {
|
|
90
|
+
try {
|
|
91
|
+
const stamp = statSync(full);
|
|
92
|
+
const key = `${stamp.size}:${stamp.mtimeMs}`;
|
|
93
|
+
if (verifiedEngineBins.get(full) === key) return true;
|
|
94
|
+
const digestFile = join(dir, ENGINE_DIGEST_FILE);
|
|
95
|
+
let recorded = null;
|
|
96
|
+
try {
|
|
97
|
+
recorded = readFileSync(digestFile, 'utf8').trim().toLowerCase();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error?.code !== 'ENOENT') return false;
|
|
100
|
+
}
|
|
101
|
+
if (recorded === null && !(await fileHasDataAsync(full))) return false;
|
|
102
|
+
const actual = await sha256File(full);
|
|
103
|
+
if (recorded === null) {
|
|
104
|
+
recordEngineDigest(digestFile, actual);
|
|
105
|
+
recorded = actual;
|
|
106
|
+
}
|
|
107
|
+
if (!validSha256(recorded) || actual !== recorded) return false;
|
|
108
|
+
verifiedEngineBins.set(full, key);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** An already-installed managed binary for `id`, or null. No network; the
|
|
116
|
+
* binary probe is async so a slow (scanned) open never stalls the event loop. */
|
|
117
|
+
export async function managedEngineBinary({ id, manifest, pluginData }) {
|
|
50
118
|
const found = manifestAsset(manifest, id);
|
|
51
119
|
if (!found || !pluginData) return null;
|
|
52
120
|
const binPath = String(found.asset.binPath || '');
|
|
53
121
|
if (!binPath) return null;
|
|
54
|
-
const
|
|
55
|
-
|
|
122
|
+
const dir = managedEngineDir(pluginData, id, found.version);
|
|
123
|
+
const full = join(dir, binPath);
|
|
124
|
+
return (await engineBinaryIntact(dir, full)) ? { path: full, version: found.version } : null;
|
|
56
125
|
}
|
|
57
126
|
|
|
58
127
|
/** Throws unless `filePath` hashes to `expected`. */
|
|
@@ -72,7 +141,13 @@ export async function verifyDownloadDigest(filePath, expected, label = LABEL) {
|
|
|
72
141
|
* Decide what an install call may do, without touching the network.
|
|
73
142
|
* Returns { targets, present, needsApproval, errors }.
|
|
74
143
|
*/
|
|
75
|
-
export function planInstall({
|
|
144
|
+
export async function planInstall({
|
|
145
|
+
ids = [],
|
|
146
|
+
manifest,
|
|
147
|
+
pluginData = '',
|
|
148
|
+
policy = 'auto',
|
|
149
|
+
approveDownloads = false,
|
|
150
|
+
} = {}) {
|
|
76
151
|
const targets = [];
|
|
77
152
|
const present = [];
|
|
78
153
|
const errors = [];
|
|
@@ -101,7 +176,7 @@ export function planInstall({ ids = [], manifest, pluginData = '', policy = 'aut
|
|
|
101
176
|
});
|
|
102
177
|
continue;
|
|
103
178
|
}
|
|
104
|
-
const installed = managedEngineBinary({ id, manifest, pluginData });
|
|
179
|
+
const installed = await managedEngineBinary({ id, manifest, pluginData });
|
|
105
180
|
if (installed) {
|
|
106
181
|
present.push({ id, version: installed.version, path: installed.path });
|
|
107
182
|
continue;
|
|
@@ -242,8 +317,9 @@ export async function installEngine(
|
|
|
242
317
|
const engineRoot = join(managedToolsDir(pluginData), target.id);
|
|
243
318
|
const finalDir = managedEngineDir(pluginData, target.id, target.version);
|
|
244
319
|
const binFull = join(finalDir, target.binPath);
|
|
245
|
-
// An engine binary a crash left NUL-filled
|
|
246
|
-
|
|
320
|
+
// An engine binary a crash left NUL-filled, or that no longer matches its
|
|
321
|
+
// recorded digest, is not present: it is installed again.
|
|
322
|
+
if (await engineBinaryIntact(finalDir, binFull)) {
|
|
247
323
|
return { id: target.id, version: target.version, path: binFull, bytes: 0, status: 'present' };
|
|
248
324
|
}
|
|
249
325
|
mkdirSync(engineRoot, { recursive: true });
|
|
@@ -272,12 +348,16 @@ export async function installEngine(
|
|
|
272
348
|
/* best-effort */
|
|
273
349
|
}
|
|
274
350
|
}
|
|
351
|
+
writeFileSync(join(stageDir, ENGINE_DIGEST_FILE), await sha256File(stagedBin));
|
|
275
352
|
try {
|
|
276
|
-
if (existsSync(finalDir) && !
|
|
353
|
+
if (existsSync(finalDir) && !(await engineBinaryIntact(finalDir, binFull))) {
|
|
354
|
+
verifiedEngineBins.delete(binFull);
|
|
355
|
+
rmSync(finalDir, { recursive: true, force: true });
|
|
356
|
+
}
|
|
277
357
|
renameSync(stageDir, finalDir);
|
|
278
358
|
} catch (error) {
|
|
279
|
-
// A concurrent install won the race
|
|
280
|
-
if (!
|
|
359
|
+
// A concurrent install won the race only if the surviving copy verifies.
|
|
360
|
+
if (!(await engineBinaryIntact(finalDir, binFull))) throw error;
|
|
281
361
|
}
|
|
282
362
|
return { id: target.id, version: target.version, path: binFull, bytes, status: 'installed' };
|
|
283
363
|
} finally {
|
|
@@ -305,7 +385,7 @@ export async function installEngines({
|
|
|
305
385
|
stallTimeoutMs = undefined,
|
|
306
386
|
} = {}) {
|
|
307
387
|
const loaded = manifest || readEnginesManifest();
|
|
308
|
-
const plan = planInstall({ ids, manifest: loaded, pluginData, policy, approveDownloads });
|
|
388
|
+
const plan = await planInstall({ ids, manifest: loaded, pluginData, policy, approveDownloads });
|
|
309
389
|
const installed = [...plan.present];
|
|
310
390
|
const errors = [...plan.errors];
|
|
311
391
|
if (!pluginData && plan.targets.length > 0) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// on the id. AUX_LANGUAGE_EXTENSIONS adds the config/markup languages the graph
|
|
7
7
|
// never parses but formatters do (PowerShell, JSON, TOML, ...).
|
|
8
8
|
import { extname } from 'node:path';
|
|
9
|
+
import { startGit } from '../shared/git-launch.mjs';
|
|
9
10
|
import { runProcess } from './process.mjs';
|
|
10
11
|
|
|
11
12
|
export const CODE_LANGUAGE_EXTENSIONS = Object.freeze({
|
|
@@ -156,21 +157,22 @@ export function parseGraphLangs(stdout) {
|
|
|
156
157
|
|
|
157
158
|
/** git-tracked files under `cwd`, filtered to the requested scope paths. */
|
|
158
159
|
async function listScopedFiles({ cwd, paths = [], signal = null, timeoutMs = 20_000 } = {}) {
|
|
159
|
-
const
|
|
160
|
-
'
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{ cwd, signal, timeoutMs }
|
|
160
|
+
const args = [
|
|
161
|
+
'-c',
|
|
162
|
+
'core.quotepath=false',
|
|
163
|
+
'--literal-pathspecs',
|
|
164
|
+
'ls-files',
|
|
165
|
+
'--cached',
|
|
166
|
+
'--others',
|
|
167
|
+
'--exclude-standard',
|
|
168
|
+
'-z',
|
|
169
|
+
'--',
|
|
170
|
+
...paths,
|
|
171
|
+
];
|
|
172
|
+
const result = await startGit(
|
|
173
|
+
process.env,
|
|
174
|
+
(command, env) => runProcess(command, args, { cwd, signal, timeoutMs, env }),
|
|
175
|
+
(outcome) => ({ code: outcome.errorCode })
|
|
174
176
|
);
|
|
175
177
|
if (result.code !== 0) {
|
|
176
178
|
return {
|