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
|
@@ -104,9 +104,10 @@ async function runGit(
|
|
|
104
104
|
maxBytes = COMMAND_MAX_BYTES,
|
|
105
105
|
allowFailure = false,
|
|
106
106
|
stdoutEncoding = 'utf8',
|
|
107
|
+
env = null,
|
|
107
108
|
} = {}
|
|
108
109
|
) {
|
|
109
|
-
const result = await runGitOffThread(args, { cwd, input, timeoutMs, maxBytes, stdoutEncoding });
|
|
110
|
+
const result = await runGitOffThread(args, { cwd, input, env, timeoutMs, maxBytes, stdoutEncoding });
|
|
110
111
|
if (result.code !== 0 && !allowFailure) throw commandError(args, result.stderr, result.code);
|
|
111
112
|
return result;
|
|
112
113
|
}
|
|
@@ -142,6 +143,7 @@ function stateForRoot(root) {
|
|
|
142
143
|
gitDir,
|
|
143
144
|
initialized: false,
|
|
144
145
|
sourceIndexPath: null,
|
|
146
|
+
sourcePaths: null,
|
|
145
147
|
sourceIndexIdentity: null,
|
|
146
148
|
sourceTracked: new Set(),
|
|
147
149
|
lineEndingArgs: [],
|
|
@@ -233,7 +235,7 @@ function assertStateLive(state) {
|
|
|
233
235
|
throw error;
|
|
234
236
|
}
|
|
235
237
|
|
|
236
|
-
function shadowArgs(state, args) {
|
|
238
|
+
function shadowArgs(state, args, workTree = state.root) {
|
|
237
239
|
assertStateLive(state);
|
|
238
240
|
return [
|
|
239
241
|
...state.lineEndingArgs,
|
|
@@ -245,7 +247,7 @@ function shadowArgs(state, args) {
|
|
|
245
247
|
'--git-dir',
|
|
246
248
|
state.gitDir,
|
|
247
249
|
'--work-tree',
|
|
248
|
-
|
|
250
|
+
workTree,
|
|
249
251
|
...args,
|
|
250
252
|
];
|
|
251
253
|
}
|
|
@@ -259,21 +261,62 @@ async function sourceGitPath(root, suffix) {
|
|
|
259
261
|
return result.code === 0 && value ? resolve(root, value) : null;
|
|
260
262
|
}
|
|
261
263
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
/** The source repository's info/exclude, index and common directory paths
|
|
265
|
+
* from one `rev-parse`, kept for the state's lifetime once answered (the
|
|
266
|
+
* index path already was). An unanswered or unexpected reply falls back to
|
|
267
|
+
* the per-path reads, uncached, so a failure is retried as before. */
|
|
268
|
+
async function sourceGitPaths(state) {
|
|
269
|
+
if (state.sourcePaths) return state.sourcePaths;
|
|
270
|
+
const result = await runGit(
|
|
271
|
+
[
|
|
272
|
+
'rev-parse',
|
|
273
|
+
'--path-format=absolute',
|
|
274
|
+
'--git-path',
|
|
275
|
+
'info/exclude',
|
|
276
|
+
'--git-path',
|
|
277
|
+
'index',
|
|
278
|
+
'--git-common-dir',
|
|
279
|
+
],
|
|
280
|
+
{ cwd: state.root, allowFailure: true }
|
|
281
|
+
);
|
|
282
|
+
const lines = String(result.stdout || '')
|
|
283
|
+
.split(/\r?\n/)
|
|
284
|
+
.filter((line) => line !== '');
|
|
285
|
+
if (result.code === 0 && lines.length === 3 && lines.every((line) => clean(line))) {
|
|
286
|
+
const [exclude, index, commonDir] = lines.map((line) => clean(line));
|
|
287
|
+
state.sourcePaths = {
|
|
288
|
+
exclude: resolve(state.root, exclude),
|
|
289
|
+
index: resolve(state.root, index),
|
|
290
|
+
commonDir,
|
|
291
|
+
};
|
|
292
|
+
return state.sourcePaths;
|
|
293
|
+
}
|
|
294
|
+
const [exclude, index, commonDir] = await Promise.all([
|
|
295
|
+
sourceGitPath(state.root, 'info/exclude'),
|
|
296
|
+
sourceGitPath(state.root, 'index'),
|
|
297
|
+
sourceCommonDir(state.root),
|
|
298
|
+
]);
|
|
299
|
+
return { exclude, index, commonDir };
|
|
268
300
|
}
|
|
269
301
|
|
|
270
|
-
async function
|
|
302
|
+
async function sourceCommonDir(root) {
|
|
271
303
|
const result = await runGit(['rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
272
|
-
cwd:
|
|
304
|
+
cwd: root,
|
|
273
305
|
allowFailure: true,
|
|
274
306
|
});
|
|
275
307
|
const commonDir = clean(result.stdout);
|
|
276
|
-
|
|
308
|
+
return result.code === 0 && commonDir ? commonDir : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function syncSourceExclude(state, source) {
|
|
312
|
+
if (!source || !(await exists(source))) return;
|
|
313
|
+
const target = join(state.gitDir, 'info', 'exclude');
|
|
314
|
+
await mkdir(dirname(target), { recursive: true });
|
|
315
|
+
await copyFile(source, target).catch(() => {});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function syncAlternates(state, commonDir) {
|
|
319
|
+
if (!commonDir) return;
|
|
277
320
|
const sourceObjects = join(resolve(state.root, commonDir), 'objects');
|
|
278
321
|
if (!(await exists(sourceObjects))) return;
|
|
279
322
|
const alternates = [sourceObjects];
|
|
@@ -294,7 +337,7 @@ async function syncAlternates(state) {
|
|
|
294
337
|
}
|
|
295
338
|
|
|
296
339
|
async function sourceIndexIdentity(state) {
|
|
297
|
-
state.sourceIndexPath ??= await
|
|
340
|
+
state.sourceIndexPath ??= (await sourceGitPaths(state)).index;
|
|
298
341
|
if (!state.sourceIndexPath) return 'missing';
|
|
299
342
|
try {
|
|
300
343
|
const stat = await lstat(state.sourceIndexPath);
|
|
@@ -349,10 +392,14 @@ async function ensureState(state) {
|
|
|
349
392
|
'utf8'
|
|
350
393
|
);
|
|
351
394
|
}
|
|
395
|
+
// One layout read serves both syncs below. Every (re)initialisation reads the
|
|
396
|
+
// source layout afresh, so a changed `.git` indirection is never served stale.
|
|
397
|
+
state.sourcePaths = null;
|
|
398
|
+
const paths = await sourceGitPaths(state);
|
|
352
399
|
const [lineEndingArgs] = await Promise.all([
|
|
353
400
|
sourceLineEndingArgs(state.root),
|
|
354
|
-
syncSourceExclude(state),
|
|
355
|
-
syncAlternates(state),
|
|
401
|
+
syncSourceExclude(state, paths.exclude),
|
|
402
|
+
syncAlternates(state, paths.commonDir),
|
|
356
403
|
]);
|
|
357
404
|
state.lineEndingArgs = lineEndingArgs;
|
|
358
405
|
// Seed once per process, then keep this index synchronized with the live
|
|
@@ -366,7 +413,7 @@ async function ensureState(state) {
|
|
|
366
413
|
|
|
367
414
|
async function resetShadowIndex(state) {
|
|
368
415
|
state.lastCapture = null;
|
|
369
|
-
state.sourceIndexPath ??= await
|
|
416
|
+
state.sourceIndexPath ??= (await sourceGitPaths(state)).index;
|
|
370
417
|
const sourceIndex = state.sourceIndexPath;
|
|
371
418
|
const target = join(state.gitDir, 'index');
|
|
372
419
|
if (sourceIndex && (await exists(sourceIndex))) {
|
|
@@ -545,7 +592,9 @@ function withOmittedBaselines(snapshot, review) {
|
|
|
545
592
|
return { ...review, files, patch, diffTooLarge: reviewDiffTooLarge(files, patch) };
|
|
546
593
|
}
|
|
547
594
|
|
|
548
|
-
|
|
595
|
+
/** `tree` with `allEntries` (recorded first states) applied. `absent`: keys of
|
|
596
|
+
* paths known not to be in `tree`, whose removal is a no-op and runs no git. */
|
|
597
|
+
async function applyBaselineFilesUnlocked(state, tree, allEntries, { absent = null } = {}) {
|
|
549
598
|
// Omitted baselines change no tree: they are carried as metadata only.
|
|
550
599
|
const files = allEntries
|
|
551
600
|
.filter((entry) => entry?.omitted === true)
|
|
@@ -555,37 +604,257 @@ async function applyBaselineFilesUnlocked(state, tree, allEntries) {
|
|
|
555
604
|
omitted: true,
|
|
556
605
|
size: Number(entry.size) || 0,
|
|
557
606
|
}));
|
|
558
|
-
const entries = allEntries
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
try {
|
|
562
|
-
await runGit(shadowArgs(state, ['read-tree', tree]), { cwd: state.root });
|
|
563
|
-
for (const entry of entries) {
|
|
607
|
+
const entries = allEntries
|
|
608
|
+
.filter((entry) => entry?.omitted !== true)
|
|
609
|
+
.map((entry) => {
|
|
564
610
|
const path = safeRelativePath(state.root, entry.path);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
611
|
+
const content = entry.content !== undefined && entry.content !== null ? entry.content : null;
|
|
612
|
+
const noop = content === null && !entry.oid && Boolean(absent?.has(pathKey(path)));
|
|
613
|
+
return { path, content, oid: entry.oid || null, noop };
|
|
614
|
+
});
|
|
615
|
+
if (entries.every((entry) => entry.noop)) {
|
|
616
|
+
return { tree, files: [...files, ...entries.map(({ path }) => ({ path, oid: null }))] };
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
return await applyInIndex(state, tree, entries, files, temporaryIndexEnv(state), { batch: true });
|
|
620
|
+
} catch {
|
|
621
|
+
// The original sequence in the shadow index: it fails, if at all, as before.
|
|
622
|
+
return await applyInShadowIndex(state, tree, entries, files);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let temporaryIndexSeq = 0;
|
|
627
|
+
|
|
628
|
+
/** An environment whose git index is a fresh file in the shadow repository. */
|
|
629
|
+
function temporaryIndexEnv(state) {
|
|
630
|
+
const index = join(state.gitDir, `index.baseline-${process.pid}-${++temporaryIndexSeq}`);
|
|
631
|
+
return { ...process.env, GIT_INDEX_FILE: index };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** `content` written as a blob, converted as the file at `path` would be. */
|
|
635
|
+
async function hashBlob(state, path, content, env) {
|
|
636
|
+
const result = await runGit(shadowArgs(state, ['hash-object', '-w', '--stdin', '--path', path]), {
|
|
637
|
+
cwd: state.root,
|
|
638
|
+
input: content,
|
|
639
|
+
env,
|
|
640
|
+
});
|
|
641
|
+
return clean(result.stdout);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
let blobWorkSeq = 0;
|
|
645
|
+
// A path component a scratch worktree file could not carry unchanged: a
|
|
646
|
+
// character or name the platform rewrites (Windows device names, trailing dots
|
|
647
|
+
// and spaces, 8.3 aliases) or git treats specially (`.git`, `.gitattributes`).
|
|
648
|
+
const UNBATCHABLE_COMPONENT =
|
|
649
|
+
/[\u0000-\u001f"*:<>?|\\]|[. ]$|~\d|^(?:con|prn|aux|nul|(?:com|lpt)[0-9\u00b9\u00b2\u00b3])(?:\..*)?$|^\.(?:git|gitattributes)$/i;
|
|
650
|
+
// Longest scratch file path written: below the classic Windows MAX_PATH.
|
|
651
|
+
const MAX_BATCH_PATH = 240;
|
|
652
|
+
|
|
653
|
+
function batchKey(path) {
|
|
654
|
+
return path.normalize('NFC').toLowerCase();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Blob ids of `items` (`{ path, content }`) as `hash-object --stdin --path`
|
|
658
|
+
* gives them, in a constant number of git processes. hash-object takes its
|
|
659
|
+
* attributes, `core.autocrlf` and eol conversion from the file's path, so the
|
|
660
|
+
* contents are written to a scratch worktree at the same relative paths, with
|
|
661
|
+
* the real worktree's `.gitattributes` files beside them, and hashed there by
|
|
662
|
+
* one `hash-object --stdin-paths`: the same attribute stack, the same
|
|
663
|
+
* conversion code. A path with a `filter=` attribute (a clean driver may read
|
|
664
|
+
* the worktree) and a path the scratch worktree cannot represent exactly are
|
|
665
|
+
* hashed one by one as before. Any failure throws and the caller falls back. */
|
|
666
|
+
async function hashBlobs(state, items, env) {
|
|
667
|
+
const ids = new Map();
|
|
668
|
+
const batched = [];
|
|
669
|
+
const single = [];
|
|
670
|
+
const keys = new Set();
|
|
671
|
+
const scratch = join(state.gitDir, `baseline-work-${process.pid}-${++blobWorkSeq}`);
|
|
672
|
+
for (const item of items) {
|
|
673
|
+
const key = batchKey(item.path);
|
|
674
|
+
const fits =
|
|
675
|
+
!keys.has(key) &&
|
|
676
|
+
scratch.length + 1 + item.path.length <= MAX_BATCH_PATH &&
|
|
677
|
+
!item.path.split('/').some((part) => UNBATCHABLE_COMPONENT.test(part));
|
|
678
|
+
if (fits) {
|
|
679
|
+
keys.add(key);
|
|
680
|
+
batched.push(item);
|
|
681
|
+
} else {
|
|
682
|
+
single.push(item);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (batched.length > 0) {
|
|
686
|
+
try {
|
|
687
|
+
const directories = new Set(['']);
|
|
688
|
+
for (const { path } of batched) {
|
|
689
|
+
for (let dir = dirname(path); dir !== '.'; dir = dirname(dir)) directories.add(dir);
|
|
690
|
+
}
|
|
691
|
+
await mkdir(scratch, { recursive: true });
|
|
692
|
+
await Promise.all([...directories].map((dir) => mkdir(join(scratch, dir), { recursive: true })));
|
|
693
|
+
await Promise.all([
|
|
694
|
+
...batched.map(({ path, content }) => writeFile(join(scratch, path), content)),
|
|
695
|
+
// git ignores a symbolic-link .gitattributes, so only a regular file is carried over.
|
|
696
|
+
...[...directories].map(async (dir) => {
|
|
697
|
+
const source = join(state.root, dir, '.gitattributes');
|
|
698
|
+
if ((await lstat(source).catch(() => null))?.isFile()) await copyFile(source, join(scratch, dir, '.gitattributes'));
|
|
699
|
+
}),
|
|
700
|
+
]);
|
|
701
|
+
const options = { cwd: scratch, env };
|
|
702
|
+
const filtered = await runGit(shadowArgs(state, ['check-attr', '-z', '--stdin', 'filter'], scratch), {
|
|
703
|
+
...options,
|
|
704
|
+
input: `${batched.map(({ path }) => path).join('\0')}\0`,
|
|
705
|
+
});
|
|
706
|
+
const fields = filtered.stdout.split('\0');
|
|
707
|
+
const driven = new Set();
|
|
708
|
+
for (let index = 0; index + 2 < fields.length; index += 3) {
|
|
709
|
+
if (fields[index + 2] !== 'unspecified') driven.add(fields[index]);
|
|
710
|
+
}
|
|
711
|
+
const plain = batched.filter((item) => {
|
|
712
|
+
if (!driven.has(item.path)) return true;
|
|
713
|
+
single.push(item);
|
|
714
|
+
return false;
|
|
715
|
+
});
|
|
716
|
+
if (plain.length > 0) {
|
|
717
|
+
const hashed = await runGit(shadowArgs(state, ['hash-object', '-w', '--stdin-paths'], scratch), {
|
|
718
|
+
...options,
|
|
719
|
+
input: `${plain.map(({ path }) => path).join('\n')}\n`,
|
|
720
|
+
});
|
|
721
|
+
const oids = hashed.stdout.split('\n').filter(Boolean);
|
|
722
|
+
if (oids.length !== plain.length) throw new Error('hash-object answered a different number of blobs');
|
|
723
|
+
plain.forEach((item, index) => ids.set(item, oids[index].trim()));
|
|
724
|
+
}
|
|
725
|
+
} finally {
|
|
726
|
+
await rm(scratch, { recursive: true, force: true }).catch(() => {});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
for (const item of single) ids.set(item, await hashBlob(state, item.path, item.content, env));
|
|
730
|
+
return ids;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Longest ls-tree command line the directory/file probe sends.
|
|
734
|
+
const MAX_PROBE_CHARS = 24_000;
|
|
735
|
+
|
|
736
|
+
/** Every proper parent directory of `path`, outermost last. */
|
|
737
|
+
function parentDirectories(path) {
|
|
738
|
+
const parents = [];
|
|
739
|
+
for (let dir = dirname(path); dir !== '.' && dir !== '/'; dir = dirname(dir)) parents.push(dir);
|
|
740
|
+
return parents;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Could `update-index --index-info` take `addPaths` on top of `tree` without
|
|
744
|
+
* replacing anything? Unlike `--cacheinfo`, it replaces a file by a directory
|
|
745
|
+
* of the same name and the reverse; the per-file sequence refuses both and
|
|
746
|
+
* the caller must see that refusal. False for a pair of added paths where one
|
|
747
|
+
* is a directory of the other, or an added path whose parent is a file (or
|
|
748
|
+
* submodule) in `tree`, or that is a directory in `tree`. A path the probe
|
|
749
|
+
* cannot ask about exactly (a leading ':' is pathspec magic) or too many to
|
|
750
|
+
* ask at once counts as unsafe, so the caller runs the per-file sequence. */
|
|
751
|
+
async function addsReplaceNothing(state, tree, addPaths, env) {
|
|
752
|
+
if (addPaths.length === 0) return true;
|
|
753
|
+
if (addPaths.some((path) => path.startsWith(':'))) return false;
|
|
754
|
+
const added = new Set(addPaths);
|
|
755
|
+
const parents = new Set(addPaths.flatMap(parentDirectories));
|
|
756
|
+
if ([...parents].some((dir) => added.has(dir))) return false;
|
|
757
|
+
const asked = [...new Set([...added, ...parents])];
|
|
758
|
+
if (asked.reduce((total, path) => total + path.length + 1, 0) > MAX_PROBE_CHARS) return false;
|
|
759
|
+
const listed = await runGit(shadowArgs(state, ['ls-tree', '-z', tree, '--', ...asked]), {
|
|
760
|
+
cwd: state.root,
|
|
761
|
+
env,
|
|
762
|
+
});
|
|
763
|
+
for (const record of listed.stdout.split('\0')) {
|
|
764
|
+
const tab = record.indexOf('\t');
|
|
765
|
+
if (tab < 0) continue;
|
|
766
|
+
const isTree = record.slice(0, tab).split(' ')[1] === 'tree';
|
|
767
|
+
const path = record.slice(tab + 1);
|
|
768
|
+
if (isTree ? added.has(path) : parents.has(path)) return false;
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** The entries applied on top of `tree` in an index of their own, removed
|
|
774
|
+
* afterwards: the shadow index is never touched, so it needs no save and
|
|
775
|
+
* restore. Every command (hash-object's attribute lookup included) sees the
|
|
776
|
+
* same index content the original sequence does: `tree` plus the entries so
|
|
777
|
+
* far. Null `env`: the shadow index itself (the caller saves and restores).
|
|
778
|
+
* `batch`: when it spawns fewer git processes and no entry swaps a file for a
|
|
779
|
+
* directory (addsReplaceNothing), every blob is hashed together (hashBlobs)
|
|
780
|
+
* and every add and removal is one `update-index --index-info`, instead of a
|
|
781
|
+
* `hash-object` and an `update-index` per entry. */
|
|
782
|
+
async function applyInIndex(state, tree, entries, baseFiles, env, { batch = false } = {}) {
|
|
783
|
+
const files = [...baseFiles];
|
|
784
|
+
try {
|
|
785
|
+
const live = entries.filter(({ noop }) => !noop);
|
|
786
|
+
const contents = live.filter(({ content }) => content !== null).length;
|
|
787
|
+
const adds = live.filter(({ content, oid }) => content !== null || oid).map(({ path }) => path);
|
|
788
|
+
// Per entry: a hash (content) plus an update-index, or an update-index. In
|
|
789
|
+
// batch: check-attr and hash-object for all contents, an ls-tree probe
|
|
790
|
+
// when anything is added, and one update-index.
|
|
791
|
+
const worthBatching =
|
|
792
|
+
batch && (contents > 0 ? 2 : 0) + 1 + (adds.length > 0 ? 1 : 0) < 2 * contents + (live.length - contents);
|
|
793
|
+
const [, replacesNothing] = await Promise.all([
|
|
794
|
+
runGit(shadowArgs(state, ['read-tree', tree]), { cwd: state.root, env }),
|
|
795
|
+
worthBatching ? addsReplaceNothing(state, tree, adds, env) : false,
|
|
796
|
+
]);
|
|
797
|
+
if (replacesNothing) {
|
|
798
|
+
const hashed = await hashBlobs(
|
|
799
|
+
state,
|
|
800
|
+
live.filter(({ content }) => content !== null),
|
|
801
|
+
env
|
|
802
|
+
);
|
|
803
|
+
const nullOid = '0'.repeat(tree.length);
|
|
804
|
+
let info = '';
|
|
805
|
+
for (const entry of entries) {
|
|
806
|
+
if (entry.noop) {
|
|
807
|
+
files.push({ path: entry.path, oid: null });
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const oid = entry.content !== null ? hashed.get(entry) : entry.oid;
|
|
811
|
+
// Mode 0 removes the path, as `update-index --force-remove` did.
|
|
812
|
+
info += oid ? `100644 ${oid}\t${entry.path}\0` : `0 ${nullOid}\t${entry.path}\0`;
|
|
813
|
+
files.push({ path: entry.path, oid });
|
|
814
|
+
}
|
|
815
|
+
await runGit(shadowArgs(state, ['update-index', '-z', '--index-info']), {
|
|
816
|
+
cwd: state.root,
|
|
817
|
+
env,
|
|
818
|
+
input: info,
|
|
819
|
+
});
|
|
820
|
+
entries = [];
|
|
821
|
+
}
|
|
822
|
+
for (const { path, content, oid: recorded, noop } of entries) {
|
|
823
|
+
if (noop) {
|
|
824
|
+
files.push({ path, oid: null });
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
let oid = recorded;
|
|
828
|
+
if (content !== null) {
|
|
829
|
+
oid = await hashBlob(state, path, content, env);
|
|
575
830
|
}
|
|
576
831
|
if (oid) {
|
|
577
832
|
await runGit(shadowArgs(state, ['update-index', '--add', '--cacheinfo', '100644', oid, path]), {
|
|
578
833
|
cwd: state.root,
|
|
834
|
+
env,
|
|
579
835
|
});
|
|
580
836
|
} else {
|
|
581
|
-
await runGit(shadowArgs(state, ['update-index', '--force-remove', '--', path]), { cwd: state.root });
|
|
837
|
+
await runGit(shadowArgs(state, ['update-index', '--force-remove', '--', path]), { cwd: state.root, env });
|
|
582
838
|
}
|
|
583
839
|
files.push({ path, oid });
|
|
584
840
|
}
|
|
585
841
|
return {
|
|
586
|
-
tree: clean((await runGit(shadowArgs(state, ['write-tree']), { cwd: state.root })).stdout),
|
|
842
|
+
tree: clean((await runGit(shadowArgs(state, ['write-tree']), { cwd: state.root, env })).stdout),
|
|
587
843
|
files,
|
|
588
844
|
};
|
|
845
|
+
} finally {
|
|
846
|
+
if (env) {
|
|
847
|
+
await rm(env.GIT_INDEX_FILE, { force: true }).catch(() => {});
|
|
848
|
+
await rm(`${env.GIT_INDEX_FILE}.lock`, { force: true }).catch(() => {});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** The original sequence: the shadow index saved as a tree, used, restored. */
|
|
854
|
+
async function applyInShadowIndex(state, tree, entries, files) {
|
|
855
|
+
const saved = clean((await runGit(shadowArgs(state, ['write-tree']), { cwd: state.root })).stdout);
|
|
856
|
+
try {
|
|
857
|
+
return await applyInIndex(state, tree, entries, files, null);
|
|
589
858
|
} finally {
|
|
590
859
|
await runGit(shadowArgs(state, ['read-tree', saved]), { cwd: state.root });
|
|
591
860
|
}
|
|
@@ -603,13 +872,26 @@ async function materializeToolBaselines(snapshot) {
|
|
|
603
872
|
}
|
|
604
873
|
);
|
|
605
874
|
const present = new Set(splitNulList(listed.stdout).map(pathKey));
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
875
|
+
// A leading ':' is pathspec magic to ls-tree and check-ignore: such a path
|
|
876
|
+
// may not have been read literally, so every read below runs as before.
|
|
877
|
+
const literal = paths.every((path) => !path.startsWith(':'));
|
|
878
|
+
// The ignore answer is only consulted for bytes kept for a path that is
|
|
879
|
+
// neither in the baseline tree nor tracked, and small enough to keep.
|
|
880
|
+
const asksIgnored = ([path, content]) =>
|
|
881
|
+
Buffer.isBuffer(content) &&
|
|
882
|
+
!present.has(pathKey(path)) &&
|
|
883
|
+
!state.sourceTracked.has(pathKey(path)) &&
|
|
884
|
+
content.length <= MAX_UNTRACKED_FILE_BYTES;
|
|
885
|
+
let ignored = new Set();
|
|
886
|
+
if (!literal || pending.some(asksIgnored)) {
|
|
887
|
+
const ignoredResult = await runGit(['check-ignore', '--no-index', '-z', '--stdin'], {
|
|
888
|
+
cwd: state.root,
|
|
889
|
+
input: `${paths.join('\0')}\0`,
|
|
890
|
+
allowFailure: true,
|
|
891
|
+
});
|
|
892
|
+
if (ignoredResult.code > 1) throw commandError(['check-ignore'], ignoredResult.stderr, ignoredResult.code);
|
|
893
|
+
ignored = new Set(splitNulList(ignoredResult.stdout).map(pathKey));
|
|
894
|
+
}
|
|
613
895
|
const missing = pending
|
|
614
896
|
.filter(
|
|
615
897
|
([path, content]) =>
|
|
@@ -624,7 +906,9 @@ async function materializeToolBaselines(snapshot) {
|
|
|
624
906
|
.map(([path, content]) =>
|
|
625
907
|
isOmittedBaseline(content) ? { path, omitted: true, size: content.size } : { path, content }
|
|
626
908
|
);
|
|
627
|
-
|
|
909
|
+
// A path ls-tree read literally and did not list is not in the tree.
|
|
910
|
+
const absent = literal ? new Set(paths.map(pathKey).filter((key) => !present.has(key))) : null;
|
|
911
|
+
const applied = await applyBaselineFilesUnlocked(state, snapshot.baselineTree, missing, { absent });
|
|
628
912
|
snapshot.baselineTree = applied.tree;
|
|
629
913
|
for (const entry of applied.files) snapshot.baselineFiles.set(pathKey(entry.path), entry);
|
|
630
914
|
for (const [path] of pending) snapshot.pendingBaselines.delete(path);
|
|
@@ -710,14 +994,68 @@ async function patchSectionsByPath(state, baselineTree, currentTree, pathspec, m
|
|
|
710
994
|
if (error?.code === 'EMAXBUFFER') return null;
|
|
711
995
|
throw error;
|
|
712
996
|
}
|
|
997
|
+
return sectionsByPath(result.stdout);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function sectionsByPath(patchText) {
|
|
713
1001
|
const sections = new Map();
|
|
714
|
-
for (const section of splitPatchSections(Buffer.from(
|
|
1002
|
+
for (const section of splitPatchSections(Buffer.from(patchText, 'utf8'))) {
|
|
715
1003
|
const path = section.newPath ?? section.oldPath;
|
|
716
1004
|
if (path) sections.set(pathKey(path), section.patch);
|
|
717
1005
|
}
|
|
718
1006
|
return sections;
|
|
719
1007
|
}
|
|
720
1008
|
|
|
1009
|
+
/** A range's name-status, numstat and whole patch from one `git diff`
|
|
1010
|
+
* (`--raw --numstat --patch -z`: raw records, numstat records, a NUL, then
|
|
1011
|
+
* the same patch bytes a patch-only read prints). Null when that read fails
|
|
1012
|
+
* or passes the patch read's byte cap, or its output is not that shape: the
|
|
1013
|
+
* caller then takes the separate reads, which fail or bound exactly as
|
|
1014
|
+
* before. */
|
|
1015
|
+
async function combinedTreeDiff(state, rangeArgs) {
|
|
1016
|
+
let result;
|
|
1017
|
+
try {
|
|
1018
|
+
result = await runGit(
|
|
1019
|
+
shadowArgs(state, ['diff', '--raw', '--numstat', '--patch', '--no-ext-diff', '--unified=3', '-z', ...rangeArgs]),
|
|
1020
|
+
{ cwd: state.root, maxBytes: COMMAND_MAX_BYTES + PATCH_OUTPUT_SLACK_BYTES, allowFailure: true }
|
|
1021
|
+
);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
if (error?.code === 'EMAXBUFFER') return null;
|
|
1024
|
+
throw error;
|
|
1025
|
+
}
|
|
1026
|
+
if (result.code !== 0) return null;
|
|
1027
|
+
const text = String(result.stdout || '');
|
|
1028
|
+
if (!text) return { statuses: new Map(), stats: new Map(), sections: new Map() };
|
|
1029
|
+
// Raw records (each starts with ':') re-shaped as name-status tokens.
|
|
1030
|
+
const nameStatus = [];
|
|
1031
|
+
let cursor = 0;
|
|
1032
|
+
const token = () => {
|
|
1033
|
+
const end = text.indexOf('\0', cursor);
|
|
1034
|
+
if (end < 0) return null;
|
|
1035
|
+
const value = text.slice(cursor, end);
|
|
1036
|
+
cursor = end + 1;
|
|
1037
|
+
return value;
|
|
1038
|
+
};
|
|
1039
|
+
while (text[cursor] === ':') {
|
|
1040
|
+
const header = token();
|
|
1041
|
+
const status = header?.slice(header.lastIndexOf(' ') + 1);
|
|
1042
|
+
if (!status) return null;
|
|
1043
|
+
const paths = [token()];
|
|
1044
|
+
if (status[0] === 'R' || status[0] === 'C') paths.push(token());
|
|
1045
|
+
if (paths.some((path) => !path)) return null;
|
|
1046
|
+
nameStatus.push(status, ...paths);
|
|
1047
|
+
}
|
|
1048
|
+
// Numstat records never hold an empty token after a NUL, so the first NUL
|
|
1049
|
+
// pair ends them; the separator NUL follows.
|
|
1050
|
+
const numstatEnd = text[cursor] === '\0' ? cursor : text.indexOf('\0\0', cursor) + 1;
|
|
1051
|
+
if (numstatEnd <= 0) return null;
|
|
1052
|
+
return {
|
|
1053
|
+
statuses: parseNameStatus(`${nameStatus.join('\0')}\0`),
|
|
1054
|
+
stats: parseNumstat(text.slice(cursor, numstatEnd)),
|
|
1055
|
+
sections: sectionsByPath(text.slice(numstatEnd + 1)),
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
721
1059
|
/** After the whole range overflowed: patches read in path batches, the
|
|
722
1060
|
* smallest changes first, until the reply's total is spent. A batch that
|
|
723
1061
|
* overflows is halved; a single file that does is too large. */
|
|
@@ -761,6 +1099,12 @@ async function boundedTreePatch(state, baselineTree, currentTree, pathspec, file
|
|
|
761
1099
|
sections = new Map();
|
|
762
1100
|
await batchedPatchSections(state, baselineTree, currentTree, files, sections);
|
|
763
1101
|
}
|
|
1102
|
+
return boundedTreePatchFromSections(files, sections, whole);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** `files` with the patch `sections` that fit the reply caps; `whole`: the
|
|
1106
|
+
* sections are a complete read, so a file without one has no patch. */
|
|
1107
|
+
function boundedTreePatchFromSections(files, sections, whole) {
|
|
764
1108
|
const budget = createReviewPatchBudget();
|
|
765
1109
|
const kept = [];
|
|
766
1110
|
const bounded = files.map((file) => {
|
|
@@ -783,12 +1127,35 @@ async function diffTreesUnlocked(state, baselineTree, currentTree, paths = null)
|
|
|
783
1127
|
// baseline describes the whole worktree, concurrent sessions included.
|
|
784
1128
|
const pathspec = Array.isArray(paths) && paths.length > 0 ? paths : ['.'];
|
|
785
1129
|
const rangeArgs = ['--find-renames', baselineTree, currentTree, '--', ...pathspec];
|
|
1130
|
+
const combined = await combinedTreeDiff(state, rangeArgs);
|
|
1131
|
+
if (combined) {
|
|
1132
|
+
const files = reviewFiles(combined.statuses, combined.stats);
|
|
1133
|
+
const bounded = boundedTreePatchFromSections(files, combined.sections, true);
|
|
1134
|
+
return {
|
|
1135
|
+
patch: bounded.patch,
|
|
1136
|
+
files: bounded.files,
|
|
1137
|
+
patchTruncated: false,
|
|
1138
|
+
diffTooLarge: reviewDiffTooLarge(bounded.files, bounded.patch),
|
|
1139
|
+
currentTree,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
786
1142
|
const [nameStatus, numstat] = await Promise.all([
|
|
787
1143
|
runGit(shadowArgs(state, ['diff', '--name-status', '-z', ...rangeArgs]), { cwd: state.root }),
|
|
788
1144
|
runGit(shadowArgs(state, ['diff', '--numstat', '-z', ...rangeArgs]), { cwd: state.root }),
|
|
789
1145
|
]);
|
|
790
|
-
const
|
|
791
|
-
const
|
|
1146
|
+
const files = reviewFiles(parseNameStatus(nameStatus.stdout), parseNumstat(numstat.stdout));
|
|
1147
|
+
const bounded = await boundedTreePatch(state, baselineTree, currentTree, pathspec, files);
|
|
1148
|
+
return {
|
|
1149
|
+
patch: bounded.patch,
|
|
1150
|
+
files: bounded.files,
|
|
1151
|
+
patchTruncated: false,
|
|
1152
|
+
diffTooLarge: reviewDiffTooLarge(bounded.files, bounded.patch),
|
|
1153
|
+
currentTree,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** Review rows from the name-status and numstat reads of one range. */
|
|
1158
|
+
function reviewFiles(statuses, stats) {
|
|
792
1159
|
const keys = [...new Set([...statuses.keys(), ...stats.keys()])];
|
|
793
1160
|
const files = keys
|
|
794
1161
|
.map((key) => {
|
|
@@ -804,14 +1171,7 @@ async function diffTreesUnlocked(state, baselineTree, currentTree, paths = null)
|
|
|
804
1171
|
};
|
|
805
1172
|
})
|
|
806
1173
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
807
|
-
|
|
808
|
-
return {
|
|
809
|
-
patch: bounded.patch,
|
|
810
|
-
files: bounded.files,
|
|
811
|
-
patchTruncated: false,
|
|
812
|
-
diffTooLarge: reviewDiffTooLarge(bounded.files, bounded.patch),
|
|
813
|
-
currentTree,
|
|
814
|
-
};
|
|
1174
|
+
return files;
|
|
815
1175
|
}
|
|
816
1176
|
|
|
817
1177
|
/** One file's patch between two trees (a rename read with its old path), up
|