create-walle 0.9.21 → 0.9.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -5
- package/package.json +2 -2
- package/template/CLAUDE.md +2 -2
- package/template/LICENSE +1 -1
- package/template/bin/ctm-dev-cleanup.js +24 -3
- package/template/bin/ctm-launch.sh +13 -0
- package/template/bin/dev.sh +156 -18
- package/template/bin/node-bin.sh +84 -0
- package/template/bin/pin-node.sh +51 -0
- package/template/claude-task-manager/api-prompts.js +1203 -182
- package/template/claude-task-manager/api-reviews.js +109 -15
- package/template/claude-task-manager/approval-agent.js +1360 -280
- package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
- package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
- package/template/claude-task-manager/db.js +4417 -295
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
- package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
- package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
- package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
- package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
- package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
- package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
- package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
- package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
- package/template/claude-task-manager/docs/phone-access-design.md +53 -15
- package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
- package/template/claude-task-manager/docs/phone-setup.md +3 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
- package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
- package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
- package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
- package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
- package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
- package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
- package/template/claude-task-manager/docs/session-title-authority.md +32 -0
- package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
- package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
- package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
- package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
- package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
- package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
- package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
- package/template/claude-task-manager/git-utils.js +897 -27
- package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
- package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
- package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
- package/template/claude-task-manager/lib/agent-presets.js +17 -1
- package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
- package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
- package/template/claude-task-manager/lib/async-semaphore.js +44 -0
- package/template/claude-task-manager/lib/auth-context.js +5 -0
- package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
- package/template/claude-task-manager/lib/auth-rules.js +29 -2
- package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
- package/template/claude-task-manager/lib/background-llm.js +144 -17
- package/template/claude-task-manager/lib/branch-inventory.js +212 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
- package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
- package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
- package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
- package/template/claude-task-manager/lib/codex-zst.js +124 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
- package/template/claude-task-manager/lib/connection-health.js +232 -0
- package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
- package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
- package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
- package/template/claude-task-manager/lib/document-review.js +141 -6
- package/template/claude-task-manager/lib/escalation-review.js +152 -0
- package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
- package/template/claude-task-manager/lib/headless-term-service.js +678 -0
- package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
- package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
- package/template/claude-task-manager/lib/main-db-census.js +216 -0
- package/template/claude-task-manager/lib/message-pagination.js +106 -4
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
- package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
- package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
- package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
- package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
- package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
- package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
- package/template/claude-task-manager/lib/perf-tracker.js +242 -6
- package/template/claude-task-manager/lib/permission-match.js +76 -0
- package/template/claude-task-manager/lib/permission-sync.js +133 -20
- package/template/claude-task-manager/lib/process-title.js +35 -0
- package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
- package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
- package/template/claude-task-manager/lib/prompt-intent.js +132 -0
- package/template/claude-task-manager/lib/provider-user-context.js +34 -0
- package/template/claude-task-manager/lib/read-pool-client.js +313 -0
- package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
- package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
- package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
- package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
- package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
- package/template/claude-task-manager/lib/restart-guard.js +109 -0
- package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
- package/template/claude-task-manager/lib/restore-policy.js +13 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
- package/template/claude-task-manager/lib/restore-runtime.js +68 -0
- package/template/claude-task-manager/lib/restore-storm.js +34 -0
- package/template/claude-task-manager/lib/resume-cwd.js +36 -0
- package/template/claude-task-manager/lib/resume-preflight.js +313 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
- package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
- package/template/claude-task-manager/lib/scheduler.js +21 -1
- package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
- package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
- package/template/claude-task-manager/lib/server-listeners.js +239 -0
- package/template/claude-task-manager/lib/session-capture.js +42 -7
- package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
- package/template/claude-task-manager/lib/session-history.js +388 -43
- package/template/claude-task-manager/lib/session-host-manager.js +287 -0
- package/template/claude-task-manager/lib/session-image-refs.js +209 -0
- package/template/claude-task-manager/lib/session-jobs.js +399 -59
- package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
- package/template/claude-task-manager/lib/session-restore.js +53 -0
- package/template/claude-task-manager/lib/session-standup.js +123 -23
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +64 -16
- package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
- package/template/claude-task-manager/lib/session-token-usage.js +494 -0
- package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
- package/template/claude-task-manager/lib/setup-network-config.js +9 -0
- package/template/claude-task-manager/lib/size-cap.js +45 -0
- package/template/claude-task-manager/lib/size-cap.test.js +62 -0
- package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
- package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
- package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
- package/template/claude-task-manager/lib/standup-attention.js +7 -3
- package/template/claude-task-manager/lib/status-authority.js +39 -0
- package/template/claude-task-manager/lib/status-hooks.js +4 -0
- package/template/claude-task-manager/lib/storage-migration.js +235 -0
- package/template/claude-task-manager/lib/structured-capture.js +298 -0
- package/template/claude-task-manager/lib/sync-io-census.js +163 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
- package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
- package/template/claude-task-manager/lib/terminal-choice.js +364 -0
- package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
- package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
- package/template/claude-task-manager/lib/timeline-order.js +122 -0
- package/template/claude-task-manager/lib/transcript-store.js +348 -43
- package/template/claude-task-manager/lib/transport-security.js +84 -1
- package/template/claude-task-manager/lib/wait-state.js +184 -0
- package/template/claude-task-manager/lib/walle-client.js +47 -5
- package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
- package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
- package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
- package/template/claude-task-manager/lib/walle-native-health.js +403 -0
- package/template/claude-task-manager/lib/walle-repair.js +701 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
- package/template/claude-task-manager/lib/walle-session-context.js +57 -21
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
- package/template/claude-task-manager/lib/walle-transcript.js +52 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
- package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/prompt-harvest.js +89 -66
- package/template/claude-task-manager/providers/claude-code.js +51 -3
- package/template/claude-task-manager/providers/cursor.js +140 -45
- package/template/claude-task-manager/public/css/reviews.css +551 -61
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +154 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18516 -2058
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +301 -0
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +901 -133
- package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
- package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
- package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
- package/template/claude-task-manager/public/js/setup.js +1273 -176
- package/template/claude-task-manager/public/js/stream-view.js +691 -73
- package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
- package/template/claude-task-manager/public/js/walle-session.js +2455 -158
- package/template/claude-task-manager/public/js/walle.js +455 -28
- package/template/claude-task-manager/public/m/app.css +2909 -262
- package/template/claude-task-manager/public/m/app.js +6601 -398
- package/template/claude-task-manager/public/m/claim.html +224 -17
- package/template/claude-task-manager/public/m/index.html +117 -21
- package/template/claude-task-manager/public/m/sw.js +3 -1
- package/template/claude-task-manager/public/manifest.json +2 -2
- package/template/claude-task-manager/public/prompts.html +30 -14
- package/template/claude-task-manager/queue-engine.js +507 -28
- package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
- package/template/claude-task-manager/server.js +14341 -2197
- package/template/claude-task-manager/session-integrity.js +160 -18
- package/template/claude-task-manager/session-search-ranking.js +1 -0
- package/template/claude-task-manager/session-utils.js +25 -5
- package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
- package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
- package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
- package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
- package/template/claude-task-manager/workers/harvest-worker.js +9 -55
- package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
- package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
- package/template/claude-task-manager/workers/session-host-process.js +146 -0
- package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
- package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
- package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
- package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
- package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
- package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
- package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
- package/template/docs/design/markdown-review-pane.md +206 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
- package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
- package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
- package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
- package/template/docs/private-memory-and-pii-policy.md +69 -0
- package/template/package.json +2 -1
- package/template/scripts/check-private-data.js +201 -0
- package/template/shared/sqlite-owner-guard.js +30 -0
- package/template/shared/sqlite-owner-write-queue.js +225 -0
- package/template/shared/sqlite-storage-policy.js +111 -0
- package/template/shared/sqlite-write-lock.js +428 -0
- package/template/wall-e/agent-runners/claude-code.js +5 -0
- package/template/wall-e/agent.js +166 -22
- package/template/wall-e/api-walle.js +524 -70
- package/template/wall-e/auth/provider-flows.js +11 -1
- package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
- package/template/wall-e/brain.js +1614 -141
- package/template/wall-e/chat/attachment-blocks.js +96 -0
- package/template/wall-e/chat/attachments.js +2 -1
- package/template/wall-e/chat/capability-resolver.js +7 -7
- package/template/wall-e/chat/context-messages.js +28 -0
- package/template/wall-e/chat/conversation-frame.js +630 -0
- package/template/wall-e/chat/provider-messages.js +125 -0
- package/template/wall-e/chat.js +1002 -233
- package/template/wall-e/coding/acceptance-contract.js +170 -0
- package/template/wall-e/coding/acp-adapter.js +1 -1
- package/template/wall-e/coding/agent-catalog.js +3 -0
- package/template/wall-e/coding/artifact-store.js +93 -0
- package/template/wall-e/coding/capability-router.js +120 -0
- package/template/wall-e/coding/coding-run-controller.js +423 -0
- package/template/wall-e/coding/compaction-service.js +157 -12
- package/template/wall-e/coding/frontend-verification.js +258 -0
- package/template/wall-e/coding/lifecycle-hooks.js +75 -0
- package/template/wall-e/coding/local-preview-contract.js +157 -0
- package/template/wall-e/coding/permission-service.js +57 -13
- package/template/wall-e/coding/prompt-bundle.js +19 -1
- package/template/wall-e/coding/prompt-section-registry.js +227 -0
- package/template/wall-e/coding/provider-compat.js +15 -0
- package/template/wall-e/coding/runtime-events.js +224 -0
- package/template/wall-e/coding/runtime-mode.js +3 -0
- package/template/wall-e/coding/side-git-snapshot.js +160 -4
- package/template/wall-e/coding/snapshot-service.js +143 -1
- package/template/wall-e/coding/stream-processor.js +388 -34
- package/template/wall-e/coding/task-tool.js +141 -4
- package/template/wall-e/coding/tool-execution-controller.js +365 -0
- package/template/wall-e/coding/tool-registry.js +43 -5
- package/template/wall-e/coding/user-hooks.js +217 -0
- package/template/wall-e/coding-orchestrator.js +1330 -221
- package/template/wall-e/coding-prompts.js +20 -4
- package/template/wall-e/context/context-builder.js +15 -2
- package/template/wall-e/decision/confidence.js +1 -1
- package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
- package/template/wall-e/docs/external-action-controller.md +26 -6
- package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
- package/template/wall-e/embeddings.js +591 -53
- package/template/wall-e/external-action-controller.js +12 -0
- package/template/wall-e/http/auth.js +1 -0
- package/template/wall-e/http/chat-api.js +46 -11
- package/template/wall-e/http/model-admin.js +836 -34
- package/template/wall-e/lib/boot-profile.js +88 -0
- package/template/wall-e/lib/event-loop-monitor.js +93 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +130 -5
- package/template/wall-e/llm/client.js +266 -63
- package/template/wall-e/llm/default-fallback.js +382 -0
- package/template/wall-e/llm/health.js +19 -0
- package/template/wall-e/llm/message-guard.js +78 -0
- package/template/wall-e/llm/model-catalog.js +252 -1
- package/template/wall-e/llm/openai.js +26 -4
- package/template/wall-e/llm/portkey-sync.js +654 -0
- package/template/wall-e/llm/provider-error.js +30 -2
- package/template/wall-e/llm/registry.js +5 -1
- package/template/wall-e/llm/request-compat.js +67 -0
- package/template/wall-e/loops/backfill.js +79 -23
- package/template/wall-e/loops/brain-optimize.js +67 -0
- package/template/wall-e/loops/ingest.js +25 -10
- package/template/wall-e/loops/question-digest.js +160 -0
- package/template/wall-e/loops/reflect.js +6 -4
- package/template/wall-e/loops/think.js +39 -12
- package/template/wall-e/mcp-server.js +318 -36
- package/template/wall-e/memory/ctm-context-client.js +52 -14
- package/template/wall-e/memory/ctm-operational-context.js +237 -0
- package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
- package/template/wall-e/memory/ctm-session-context.js +111 -63
- package/template/wall-e/prompts/coding/deepseek.txt +3 -0
- package/template/wall-e/prompts/coding/gemini.txt +6 -0
- package/template/wall-e/prompts/coding/gpt.txt +6 -0
- package/template/wall-e/prompts/coding/local.txt +7 -0
- package/template/wall-e/runtime/decision-hooks.js +115 -0
- package/template/wall-e/runtime/devbox-gateway.js +82 -8
- package/template/wall-e/runtime/prompt-manifest.js +86 -0
- package/template/wall-e/runtime/tool-executor.js +269 -0
- package/template/wall-e/runtime/tool-result-envelope.js +138 -0
- package/template/wall-e/runtime/transcript-projection.js +60 -0
- package/template/wall-e/runtime/walle-runtime.js +224 -0
- package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
- package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
- package/template/wall-e/server.js +15 -0
- package/template/wall-e/session-files.js +9 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
- package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
- package/template/wall-e/skills/claude-code-reader.js +7 -3
- package/template/wall-e/skills/script-skill-runner.js +10 -0
- package/template/wall-e/skills/skill-planner.js +38 -0
- package/template/wall-e/tools/builtin-middleware.js +19 -9
- package/template/wall-e/tools/local-tools.js +1428 -16
- package/template/wall-e/tools/permission-checker.js +73 -5
- package/template/wall-e/tools/question-manager.js +117 -7
- package/template/wall-e/training/harvester.js +12 -28
- package/template/wall-e/training/replay.js +25 -80
- package/template/website/index.html +10 -10
- package/template/wall-e/eval/ab-test.js +0 -203
- package/template/wall-e/eval/agent-runner.js +0 -772
- package/template/wall-e/eval/agent-scorer.js +0 -461
- package/template/wall-e/eval/aggregator.js +0 -414
- package/template/wall-e/eval/allowed-test-commands.js +0 -34
- package/template/wall-e/eval/benchmark-generator.js +0 -113
- package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
- package/template/wall-e/eval/benchmarks/chat.json +0 -82
- package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
- package/template/wall-e/eval/benchmarks/coding.json +0 -122
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
- package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
- package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
- package/template/wall-e/eval/benchmarks.js +0 -669
- package/template/wall-e/eval/cc-replay.js +0 -719
- package/template/wall-e/eval/chat-eval.js +0 -525
- package/template/wall-e/eval/check-keys.js +0 -15
- package/template/wall-e/eval/check-providers.js +0 -42
- package/template/wall-e/eval/codex-cli-baseline.js +0 -669
- package/template/wall-e/eval/coding-agent-real.js +0 -570
- package/template/wall-e/eval/context-compactor.js +0 -251
- package/template/wall-e/eval/debug-agent003.js +0 -68
- package/template/wall-e/eval/diagnostics.js +0 -216
- package/template/wall-e/eval/eval-orchestrator.js +0 -642
- package/template/wall-e/eval/evaluate.js +0 -202
- package/template/wall-e/eval/evaluator.js +0 -373
- package/template/wall-e/eval/exporter.js +0 -212
- package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
- package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
- package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
- package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
- package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
- package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
- package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
- package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
- package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
- package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
- package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
- package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
- package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
- package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
- package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
- package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
- package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
- package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
- package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
- package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
- package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
- package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
- package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
- package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
- package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
- package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
- package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
- package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
- package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
- package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
- package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
- package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
- package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
- package/template/wall-e/eval/harvester.js +0 -685
- package/template/wall-e/eval/head-to-head.js +0 -388
- package/template/wall-e/eval/humaneval-adapter.js +0 -321
- package/template/wall-e/eval/list-models.js +0 -31
- package/template/wall-e/eval/livecodebench-adapter.js +0 -291
- package/template/wall-e/eval/mail-integration.js +0 -443
- package/template/wall-e/eval/manifest.js +0 -186
- package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
- package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
- package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
- package/template/wall-e/eval/meta-harness/cli.js +0 -86
- package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
- package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
- package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
- package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
- package/template/wall-e/eval/meta-harness/frontier.js +0 -96
- package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
- package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
- package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
- package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
- package/template/wall-e/eval/meta-harness/reporting.js +0 -58
- package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
- package/template/wall-e/eval/meta-harness/validation.js +0 -81
- package/template/wall-e/eval/promoter.js +0 -228
- package/template/wall-e/eval/provider-normalizer.js +0 -33
- package/template/wall-e/eval/replay.js +0 -395
- package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
- package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
- package/template/wall-e/eval/run-coding-agent-real.js +0 -187
- package/template/wall-e/eval/run-eval.js +0 -435
- package/template/wall-e/eval/run-model-comparison.js +0 -142
- package/template/wall-e/eval/session-evaluator.js +0 -187
- package/template/wall-e/eval/session-miner.js +0 -207
- package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
- package/template/wall-e/eval/session-transcripts.js +0 -509
- package/template/wall-e/eval/shadow.js +0 -161
- package/template/wall-e/eval/swebench-adapter.js +0 -345
- package/template/wall-e/eval/swebench-docker.js +0 -192
- package/template/wall-e/eval/train.py +0 -320
- package/template/wall-e/eval/trainer.js +0 -232
- package/template/wall-e/eval/weekly-eval-loop.js +0 -241
|
@@ -1,7 +1,276 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
const { execFile } = require('child_process');
|
|
2
|
+
const { execFile, spawn } = require('child_process');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
+
const DEFAULT_GIT_TIMEOUT_MS = _positiveInt(process.env.CTM_GIT_TIMEOUT_MS, 15000);
|
|
6
|
+
const DEFAULT_WORKTREE_CREATE_TIMEOUT_MS = Math.max(
|
|
7
|
+
DEFAULT_GIT_TIMEOUT_MS,
|
|
8
|
+
_positiveInt(process.env.CTM_WORKTREE_CREATE_TIMEOUT_MS, 120000)
|
|
9
|
+
);
|
|
10
|
+
const WORKTREE_CLEANUP_TIMEOUT_MS = Math.max(
|
|
11
|
+
DEFAULT_GIT_TIMEOUT_MS,
|
|
12
|
+
_positiveInt(process.env.CTM_WORKTREE_CLEANUP_TIMEOUT_MS, 60000)
|
|
13
|
+
);
|
|
14
|
+
const GIT_FORCE_KILL_DELAY_MS = 2500;
|
|
15
|
+
|
|
16
|
+
function _positiveInt(value, fallback) {
|
|
17
|
+
const n = Number(value);
|
|
18
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _normalizeGitOptions(maxBuffer, options) {
|
|
22
|
+
if (maxBuffer && typeof maxBuffer === 'object') {
|
|
23
|
+
options = options || {};
|
|
24
|
+
return {
|
|
25
|
+
maxBuffer: maxBuffer.maxBuffer || 1024 * 1024 * 5,
|
|
26
|
+
timeoutMs: _positiveInt(maxBuffer.timeoutMs ?? options.timeoutMs, DEFAULT_GIT_TIMEOUT_MS),
|
|
27
|
+
operation: maxBuffer.operation || options.operation || '',
|
|
28
|
+
timeoutCode: maxBuffer.timeoutCode || options.timeoutCode || 'GIT_COMMAND_TIMEOUT',
|
|
29
|
+
cancelCode: maxBuffer.cancelCode || options.cancelCode || 'GIT_COMMAND_CANCELLED',
|
|
30
|
+
allowStdoutOnError: !!(maxBuffer.allowStdoutOnError || options.allowStdoutOnError),
|
|
31
|
+
signal: maxBuffer.signal || options.signal || null,
|
|
32
|
+
onProgress: typeof maxBuffer.onProgress === 'function' ? maxBuffer.onProgress : options.onProgress,
|
|
33
|
+
heartbeatMs: _positiveInt(maxBuffer.heartbeatMs ?? options.heartbeatMs, 1000),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
options = options || {};
|
|
37
|
+
return {
|
|
38
|
+
maxBuffer: maxBuffer || 1024 * 1024 * 5,
|
|
39
|
+
timeoutMs: _positiveInt(options.timeoutMs, DEFAULT_GIT_TIMEOUT_MS),
|
|
40
|
+
operation: options.operation || '',
|
|
41
|
+
timeoutCode: options.timeoutCode || 'GIT_COMMAND_TIMEOUT',
|
|
42
|
+
cancelCode: options.cancelCode || 'GIT_COMMAND_CANCELLED',
|
|
43
|
+
allowStdoutOnError: !!options.allowStdoutOnError,
|
|
44
|
+
signal: options.signal || null,
|
|
45
|
+
onProgress: typeof options.onProgress === 'function' ? options.onProgress : null,
|
|
46
|
+
heartbeatMs: _positiveInt(options.heartbeatMs, 1000),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function _isExecTimeout(err) {
|
|
51
|
+
if (!err) return false;
|
|
52
|
+
if (err.timedOut === true) return true;
|
|
53
|
+
return err.killed === true
|
|
54
|
+
|| err.code === 'ETIMEDOUT'
|
|
55
|
+
|| err.signal === 'SIGTERM';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _isExecCancelled(err) {
|
|
59
|
+
if (!err) return false;
|
|
60
|
+
return err.cancelled === true || err.name === 'AbortError';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _gitCommandForMessage(args) {
|
|
64
|
+
return `git ${args.map(String).join(' ')}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _wrapGitError(err, args, opts, stdout, stderr) {
|
|
68
|
+
const cancelled = _isExecCancelled(err);
|
|
69
|
+
const timedOut = !cancelled && _isExecTimeout(err);
|
|
70
|
+
const command = _gitCommandForMessage(args);
|
|
71
|
+
const rawMessage = String(stderr || stdout || err?.message || 'git command failed').trim();
|
|
72
|
+
const message = cancelled
|
|
73
|
+
? `${opts.operation || command} was cancelled`
|
|
74
|
+
: timedOut
|
|
75
|
+
? `${opts.operation || command} timed out after ${opts.timeoutMs}ms`
|
|
76
|
+
: (rawMessage || `${command} failed`);
|
|
77
|
+
const wrapped = new Error(message);
|
|
78
|
+
wrapped.code = cancelled ? opts.cancelCode : timedOut ? opts.timeoutCode : err?.code;
|
|
79
|
+
wrapped.originalCode = err?.code;
|
|
80
|
+
wrapped.signal = err?.signal;
|
|
81
|
+
wrapped.killed = err?.killed === true;
|
|
82
|
+
wrapped.timedOut = timedOut;
|
|
83
|
+
wrapped.cancelled = cancelled;
|
|
84
|
+
wrapped.timeoutMs = opts.timeoutMs;
|
|
85
|
+
wrapped.gitArgs = args.slice();
|
|
86
|
+
wrapped.gitCommand = command;
|
|
87
|
+
wrapped.stdout = stdout || '';
|
|
88
|
+
wrapped.stderr = stderr || '';
|
|
89
|
+
return wrapped;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _emitGitProgress(onProgress, event) {
|
|
93
|
+
if (typeof onProgress !== 'function') return;
|
|
94
|
+
try {
|
|
95
|
+
onProgress(event);
|
|
96
|
+
} catch (_) {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _runGitSpawn(cwd, args, maxBuffer, options) {
|
|
100
|
+
const opts = _normalizeGitOptions(maxBuffer, options);
|
|
101
|
+
const command = _gitCommandForMessage(args);
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const startedAt = Date.now();
|
|
104
|
+
let child = null;
|
|
105
|
+
let stdout = '';
|
|
106
|
+
let stderr = '';
|
|
107
|
+
let stdoutBytes = 0;
|
|
108
|
+
let stderrBytes = 0;
|
|
109
|
+
let lastOutput = '';
|
|
110
|
+
let lastOutputAt = startedAt;
|
|
111
|
+
let settled = false;
|
|
112
|
+
let timedOut = false;
|
|
113
|
+
let cancelled = false;
|
|
114
|
+
let bufferOverflow = false;
|
|
115
|
+
let timeoutTimer = null;
|
|
116
|
+
let heartbeatTimer = null;
|
|
117
|
+
let forceKillTimer = null;
|
|
118
|
+
|
|
119
|
+
function elapsedMs() {
|
|
120
|
+
return Date.now() - startedAt;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function progress(extra) {
|
|
124
|
+
_emitGitProgress(opts.onProgress, {
|
|
125
|
+
type: 'heartbeat',
|
|
126
|
+
status: cancelled ? 'cancelling' : 'running',
|
|
127
|
+
operation: opts.operation || command,
|
|
128
|
+
gitCommand: command,
|
|
129
|
+
gitArgs: args.slice(),
|
|
130
|
+
pid: child && child.pid,
|
|
131
|
+
startedAt,
|
|
132
|
+
elapsedMs: elapsedMs(),
|
|
133
|
+
quietMs: Date.now() - lastOutputAt,
|
|
134
|
+
lastOutputAt,
|
|
135
|
+
lastOutput,
|
|
136
|
+
...(extra || {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function cleanupListeners() {
|
|
141
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
142
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
143
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
144
|
+
if (opts.signal && abortListener) {
|
|
145
|
+
try { opts.signal.removeEventListener('abort', abortListener); } catch (_) {}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function finish(err) {
|
|
150
|
+
if (settled) return;
|
|
151
|
+
settled = true;
|
|
152
|
+
cleanupListeners();
|
|
153
|
+
if (err) reject(err);
|
|
154
|
+
else resolve(stdout);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function killChild(reason) {
|
|
158
|
+
if (!child || child.killed) return;
|
|
159
|
+
progress({
|
|
160
|
+
type: reason || 'kill',
|
|
161
|
+
status: cancelled ? 'cancelling' : timedOut ? 'timeout' : 'running',
|
|
162
|
+
message: cancelled ? 'Cancel requested; stopping git checkout.' : timedOut ? 'Checkout exceeded the safety timeout; stopping git.' : 'Stopping git.',
|
|
163
|
+
});
|
|
164
|
+
try { child.kill('SIGTERM'); } catch (_) {}
|
|
165
|
+
forceKillTimer = setTimeout(() => {
|
|
166
|
+
if (!settled && child && !child.killed) {
|
|
167
|
+
try { child.kill('SIGKILL'); } catch (_) {}
|
|
168
|
+
}
|
|
169
|
+
}, GIT_FORCE_KILL_DELAY_MS);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const abortListener = () => {
|
|
173
|
+
cancelled = true;
|
|
174
|
+
killChild('cancel');
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (opts.signal && opts.signal.aborted) {
|
|
178
|
+
const err = _wrapGitError({ cancelled: true }, args, opts, stdout, stderr);
|
|
179
|
+
return finish(err);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return finish(_wrapGitError(err, args, opts, stdout, stderr));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (opts.signal) {
|
|
189
|
+
try { opts.signal.addEventListener('abort', abortListener, { once: true }); } catch (_) {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
progress({ type: 'start', message: `Started ${opts.operation || command}` });
|
|
193
|
+
|
|
194
|
+
function append(stream, chunk) {
|
|
195
|
+
if (settled) return;
|
|
196
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '');
|
|
197
|
+
const bytes = Buffer.byteLength(text);
|
|
198
|
+
if (stream === 'stdout') {
|
|
199
|
+
stdout += text;
|
|
200
|
+
stdoutBytes += bytes;
|
|
201
|
+
} else {
|
|
202
|
+
stderr += text;
|
|
203
|
+
stderrBytes += bytes;
|
|
204
|
+
}
|
|
205
|
+
const trimmed = text.trim();
|
|
206
|
+
if (trimmed) lastOutput = trimmed.split(/\r?\n/).slice(-1)[0].slice(0, 1000);
|
|
207
|
+
lastOutputAt = Date.now();
|
|
208
|
+
progress({ type: stream, stream, text: trimmed.slice(0, 2000), lastOutput });
|
|
209
|
+
if (stdoutBytes + stderrBytes > opts.maxBuffer) {
|
|
210
|
+
bufferOverflow = true;
|
|
211
|
+
killChild('maxBuffer');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
child.stdout.on('data', chunk => append('stdout', chunk));
|
|
216
|
+
child.stderr.on('data', chunk => append('stderr', chunk));
|
|
217
|
+
child.on('error', err => finish(_wrapGitError(err, args, opts, stdout, stderr)));
|
|
218
|
+
child.on('close', (code, signal) => {
|
|
219
|
+
if (bufferOverflow) {
|
|
220
|
+
const err = new Error(`${opts.operation || command} exceeded output buffer`);
|
|
221
|
+
err.code = 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
|
|
222
|
+
err.signal = signal;
|
|
223
|
+
finish(_wrapGitError(err, args, opts, stdout, stderr));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (cancelled) {
|
|
227
|
+
const err = _wrapGitError({ cancelled: true, signal, killed: true }, args, opts, stdout, stderr);
|
|
228
|
+
finish(err);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (timedOut) {
|
|
232
|
+
const err = _wrapGitError({ timedOut: true, signal, killed: true }, args, opts, stdout, stderr);
|
|
233
|
+
finish(err);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (code !== 0) {
|
|
237
|
+
const err = new Error(stderr || stdout || `${command} exited with code ${code}`);
|
|
238
|
+
err.code = code;
|
|
239
|
+
err.signal = signal;
|
|
240
|
+
finish(_wrapGitError(err, args, opts, stdout, stderr));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
progress({ type: 'complete', status: 'succeeded', message: `${opts.operation || command} completed` });
|
|
244
|
+
finish();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (opts.timeoutMs > 0) {
|
|
248
|
+
timeoutTimer = setTimeout(() => {
|
|
249
|
+
timedOut = true;
|
|
250
|
+
killChild('timeout');
|
|
251
|
+
}, opts.timeoutMs);
|
|
252
|
+
}
|
|
253
|
+
if (opts.heartbeatMs > 0) {
|
|
254
|
+
heartbeatTimer = setInterval(() => {
|
|
255
|
+
if (!settled) progress({ type: 'heartbeat' });
|
|
256
|
+
}, opts.heartbeatMs);
|
|
257
|
+
if (heartbeatTimer.unref) heartbeatTimer.unref();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function _runGit(cwd, args, maxBuffer, options) {
|
|
263
|
+
const opts = _normalizeGitOptions(maxBuffer, options);
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
execFile('git', args, { cwd, maxBuffer: opts.maxBuffer, timeout: opts.timeoutMs }, (err, stdout, stderr) => {
|
|
266
|
+
if (!err) return resolve(stdout);
|
|
267
|
+
const wrapped = _wrapGitError(err, args, opts, stdout, stderr);
|
|
268
|
+
if (opts.allowStdoutOnError && stdout && !wrapped.timedOut) return resolve(stdout);
|
|
269
|
+
reject(wrapped);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
5
274
|
function _worktreeNamespace(options) {
|
|
6
275
|
const raw = typeof options === 'string'
|
|
7
276
|
? options
|
|
@@ -16,11 +285,19 @@ function _worktreeParentDir(cwd, options) {
|
|
|
16
285
|
}
|
|
17
286
|
|
|
18
287
|
// Run a git command in a given project directory
|
|
19
|
-
function git(cwd, args, maxBuffer = 1024 * 1024 * 5) {
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
288
|
+
function git(cwd, args, maxBuffer = 1024 * 1024 * 5, options) {
|
|
289
|
+
return _runGit(cwd, args, maxBuffer, { ...(options || {}), allowStdoutOnError: true });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function gitStrict(cwd, args, maxBuffer = 1024 * 1024 * 5, options) {
|
|
293
|
+
return _runGit(cwd, args, maxBuffer, options);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function gitExitCode(cwd, args, maxBuffer = 1024 * 1024 * 5) {
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
const opts = _normalizeGitOptions(maxBuffer, {});
|
|
299
|
+
execFile('git', args, { cwd, maxBuffer: opts.maxBuffer, timeout: opts.timeoutMs }, (err) => {
|
|
300
|
+
resolve(err ? (_isExecTimeout(err) ? 124 : Number(err.code || 1)) : 0);
|
|
24
301
|
});
|
|
25
302
|
});
|
|
26
303
|
}
|
|
@@ -142,6 +419,117 @@ async function getStagedDiff(cwd) {
|
|
|
142
419
|
return parseDiff(raw);
|
|
143
420
|
}
|
|
144
421
|
|
|
422
|
+
// --- Stable per-repo git fact cache --------------------------------------
|
|
423
|
+
// resolveMainBranch / getDefaultReviewBase / getDefaultDiffStat each spawn
|
|
424
|
+
// 3-7 git subprocesses. A CPU profile of the live primary showed ~8.4s of
|
|
425
|
+
// synchronous spawn() fork/exec prefix on the main loop over ~4 min — fanned
|
|
426
|
+
// out by the Review tab/chooser polling these on demand AND the 30s
|
|
427
|
+
// file-change sweep calling getDefaultDiffStat per project. The underlying
|
|
428
|
+
// facts (main branch, merge-base, diff stat) are stable for seconds, so a
|
|
429
|
+
// short per-cwd TTL cache collapses the fan-out. The IN-FLIGHT promise is
|
|
430
|
+
// cached (not just the resolved value) so a burst of concurrent callers for
|
|
431
|
+
// the same repo shares ONE git invocation. Disable with CTM_GIT_CACHE=0.
|
|
432
|
+
const _GIT_CACHE_ENABLED = process.env.CTM_GIT_CACHE !== '0';
|
|
433
|
+
function _ttlPromiseCache(ttlMs) {
|
|
434
|
+
const m = new Map(); // key -> { at, p }
|
|
435
|
+
const get = (key, producer) => {
|
|
436
|
+
if (!_GIT_CACHE_ENABLED || ttlMs <= 0) return Promise.resolve().then(producer);
|
|
437
|
+
const now = Date.now();
|
|
438
|
+
const e = m.get(key);
|
|
439
|
+
if (e && (now - e.at) < ttlMs) return e.p;
|
|
440
|
+
const p = Promise.resolve().then(producer);
|
|
441
|
+
m.set(key, { at: now, p });
|
|
442
|
+
// Drop the entry if the producer rejects so the next call retries instead
|
|
443
|
+
// of pinning a failed result for the whole TTL window.
|
|
444
|
+
p.catch(() => { const cur = m.get(key); if (cur && cur.p === p) m.delete(key); });
|
|
445
|
+
return p;
|
|
446
|
+
};
|
|
447
|
+
get.clear = () => m.clear();
|
|
448
|
+
return get;
|
|
449
|
+
}
|
|
450
|
+
const _MAIN_BRANCH_TTL_MS = Math.max(0, Number(process.env.CTM_GIT_MAIN_BRANCH_TTL_MS) || 60000);
|
|
451
|
+
const _REVIEW_BASE_TTL_MS = Math.max(0, Number(process.env.CTM_GIT_REVIEW_BASE_TTL_MS) || 5000);
|
|
452
|
+
const _DIFF_STAT_TTL_MS = Math.max(0, Number(process.env.CTM_GIT_DIFF_STAT_TTL_MS) || 3000);
|
|
453
|
+
const _mainBranchCache = _ttlPromiseCache(_MAIN_BRANCH_TTL_MS);
|
|
454
|
+
const _reviewBaseCache = _ttlPromiseCache(_REVIEW_BASE_TTL_MS);
|
|
455
|
+
const _diffStatCache = _ttlPromiseCache(_DIFF_STAT_TTL_MS);
|
|
456
|
+
// Drop cached facts (all repos) after an operation that changes branch/HEAD/
|
|
457
|
+
// working tree so the next read reflects it immediately.
|
|
458
|
+
function _invalidateGitFactCache() {
|
|
459
|
+
_mainBranchCache.clear(); _reviewBaseCache.clear(); _diffStatCache.clear();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Resolve the repo's main branch: origin/HEAD target, else local main, else master.
|
|
463
|
+
async function resolveMainBranch(cwd) {
|
|
464
|
+
return _mainBranchCache(cwd || '', () => _resolveMainBranchUncached(cwd));
|
|
465
|
+
}
|
|
466
|
+
async function _resolveMainBranchUncached(cwd) {
|
|
467
|
+
const originHead = await _gitSafe(cwd, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD']);
|
|
468
|
+
if (originHead) {
|
|
469
|
+
const b = originHead.replace(/^origin\//, '').trim();
|
|
470
|
+
if (b) return b;
|
|
471
|
+
}
|
|
472
|
+
for (const cand of ['main', 'master']) {
|
|
473
|
+
if (await _gitSafe(cwd, ['rev-parse', '--verify', '--quiet', cand])) return cand;
|
|
474
|
+
}
|
|
475
|
+
return 'main';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Decide the default review base for a project (the PR-style "all branch work" model):
|
|
479
|
+
// - on a branch ahead of / diverged from main → diff working tree vs merge-base(main, HEAD)
|
|
480
|
+
// - on main / detached / no main → all uncommitted work (vs HEAD)
|
|
481
|
+
// Returns { kind:'vs-main'|'uncommitted', sentinel, ref, mainBranch, mergeBase }.
|
|
482
|
+
// `sentinel` is the value the client puts in crState.baseRef; `ref` is the concrete git ref.
|
|
483
|
+
async function getDefaultReviewBase(cwd) {
|
|
484
|
+
return _reviewBaseCache(cwd || '', () => _getDefaultReviewBaseUncached(cwd));
|
|
485
|
+
}
|
|
486
|
+
async function _getDefaultReviewBaseUncached(cwd) {
|
|
487
|
+
const mainBranch = await resolveMainBranch(cwd);
|
|
488
|
+
const head = await _gitSafe(cwd, ['rev-parse', 'HEAD']);
|
|
489
|
+
const mainSha = await _gitSafe(cwd, ['rev-parse', '--verify', '--quiet', mainBranch]);
|
|
490
|
+
if (head && mainSha && head !== mainSha) {
|
|
491
|
+
const mergeBase = await _gitSafe(cwd, ['merge-base', mainBranch, 'HEAD']);
|
|
492
|
+
if (mergeBase && mergeBase !== head) {
|
|
493
|
+
return { kind: 'vs-main', sentinel: '--vs-main', ref: mergeBase, mainBranch, mergeBase };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return { kind: 'uncommitted', sentinel: '--uncommitted', ref: 'HEAD', mainBranch, mergeBase: null };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Working tree vs an arbitrary ref (merge-base SHA or HEAD) — includes committed +
|
|
500
|
+
// staged + unstaged relative to that ref. Distinct from getFullDiff's commit mode,
|
|
501
|
+
// which shows a single commit's parent..commit diff.
|
|
502
|
+
async function getDiffVsRef(cwd, ref) {
|
|
503
|
+
const raw = await git(cwd, ['diff', '--relative', '-U5', ref]);
|
|
504
|
+
return parseDiff(raw);
|
|
505
|
+
}
|
|
506
|
+
async function getDiffStatVsRef(cwd, ref) {
|
|
507
|
+
const out = await git(cwd, ['diff', '--relative', '--stat', '--numstat', ref]);
|
|
508
|
+
return parseNumstat(out);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Diff-stat for the default review base (used so the chooser badge matches what opens).
|
|
512
|
+
async function getDefaultDiffStat(cwd) {
|
|
513
|
+
return _diffStatCache(cwd || '', () => _getDefaultDiffStatUncached(cwd));
|
|
514
|
+
}
|
|
515
|
+
async function _getDefaultDiffStatUncached(cwd) {
|
|
516
|
+
const base = await getDefaultReviewBase(cwd);
|
|
517
|
+
const files = await getDiffStatVsRef(cwd, base.ref);
|
|
518
|
+
return { base, files };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Read a file's full contents for the "new" side of a diff, so the UI can expand context
|
|
522
|
+
// lines hidden between hunks. For a commit SHA base the new side is the file at that commit;
|
|
523
|
+
// for staged it's the index version; otherwise (working tree / vs-main / uncommitted) it's
|
|
524
|
+
// the current working-tree file on disk.
|
|
525
|
+
async function getFileAtRef(cwd, relPath, ref) {
|
|
526
|
+
if (relPath.includes('..') || relPath.startsWith('/')) throw new Error('invalid path');
|
|
527
|
+
if (ref && /^[0-9a-f]{7,40}$/.test(ref)) return git(cwd, ['show', `${ref}:${relPath}`]);
|
|
528
|
+
if (ref === '--staged') return git(cwd, ['show', `:${relPath}`]);
|
|
529
|
+
const fs = require('fs'); const path = require('path');
|
|
530
|
+
return fs.promises.readFile(path.join(cwd, relPath), 'utf8');
|
|
531
|
+
}
|
|
532
|
+
|
|
145
533
|
// Parse unified diff format into structured data
|
|
146
534
|
function parseDiff(raw) {
|
|
147
535
|
const files = [];
|
|
@@ -305,6 +693,144 @@ async function _branchExists(cwd, branch) {
|
|
|
305
693
|
}
|
|
306
694
|
}
|
|
307
695
|
|
|
696
|
+
function _formatDuration(ms) {
|
|
697
|
+
if (ms >= 1000 && ms % 1000 === 0) return `${ms / 1000}s`;
|
|
698
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
|
|
699
|
+
return `${ms}ms`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function _cleanupPartialWorktree(cwd, worktreePath, branchName, options = {}) {
|
|
703
|
+
const fs = require('fs');
|
|
704
|
+
const cleanup = {
|
|
705
|
+
attempted: true,
|
|
706
|
+
worktreeRemoved: false,
|
|
707
|
+
pathRemoved: false,
|
|
708
|
+
pathAlreadyGone: false,
|
|
709
|
+
pruned: false,
|
|
710
|
+
branchDeleted: false,
|
|
711
|
+
errors: [],
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
await gitStrict(cwd, ['worktree', 'remove', '--force', worktreePath], 1024 * 1024, {
|
|
718
|
+
timeoutMs: WORKTREE_CLEANUP_TIMEOUT_MS,
|
|
719
|
+
operation: `cleanup partial worktree ${worktreePath}`,
|
|
720
|
+
});
|
|
721
|
+
cleanup.worktreeRemoved = true;
|
|
722
|
+
} catch (e) {
|
|
723
|
+
const notRegistered = /not a working tree|is not a working tree/i.test(String(e.message || ''));
|
|
724
|
+
if (fs.existsSync(worktreePath)) {
|
|
725
|
+
try {
|
|
726
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
727
|
+
cleanup.pathRemoved = true;
|
|
728
|
+
} catch (rmErr) {
|
|
729
|
+
cleanup.errors.push(`path remove: ${rmErr.message}`);
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
cleanup.pathAlreadyGone = true;
|
|
733
|
+
}
|
|
734
|
+
if (!notRegistered) {
|
|
735
|
+
cleanup.errors.push(`worktree remove: ${e.message}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
try {
|
|
740
|
+
await gitStrict(cwd, ['worktree', 'prune'], 1024 * 1024, {
|
|
741
|
+
timeoutMs: WORKTREE_CLEANUP_TIMEOUT_MS,
|
|
742
|
+
operation: 'prune partial worktree metadata',
|
|
743
|
+
});
|
|
744
|
+
cleanup.pruned = true;
|
|
745
|
+
} catch (e) {
|
|
746
|
+
cleanup.errors.push(`worktree prune: ${e.message}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (options.deleteBranch && branchName && await _branchExists(cwd, branchName)) {
|
|
750
|
+
try {
|
|
751
|
+
await gitStrict(cwd, ['branch', '-D', branchName], 1024 * 1024, {
|
|
752
|
+
timeoutMs: WORKTREE_CLEANUP_TIMEOUT_MS,
|
|
753
|
+
operation: `delete partial worktree branch ${branchName}`,
|
|
754
|
+
});
|
|
755
|
+
cleanup.branchDeleted = true;
|
|
756
|
+
} catch (e) {
|
|
757
|
+
cleanup.errors.push(`branch delete: ${e.message}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return cleanup;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function _gitWorktreeAdd(cwd, args, context) {
|
|
765
|
+
const timeoutMs = _positiveInt(context?.timeoutMs, DEFAULT_WORKTREE_CREATE_TIMEOUT_MS);
|
|
766
|
+
const onProgress = typeof context?.onProgress === 'function' ? context.onProgress : null;
|
|
767
|
+
const branchName = context?.branchName || '';
|
|
768
|
+
const worktreePath = context?.worktreePath || '';
|
|
769
|
+
_emitGitProgress(onProgress, {
|
|
770
|
+
stage: 'git_worktree_add',
|
|
771
|
+
status: 'running',
|
|
772
|
+
branch: branchName,
|
|
773
|
+
worktreePath,
|
|
774
|
+
message: `Starting git worktree checkout for "${branchName}".`,
|
|
775
|
+
});
|
|
776
|
+
try {
|
|
777
|
+
await _runGitSpawn(cwd, args, 1024 * 1024 * 5, {
|
|
778
|
+
timeoutMs,
|
|
779
|
+
operation: `create worktree ${branchName || ''}`.trim(),
|
|
780
|
+
timeoutCode: 'WORKTREE_CREATE_TIMEOUT',
|
|
781
|
+
cancelCode: 'WORKTREE_CREATE_CANCELLED',
|
|
782
|
+
signal: context?.signal,
|
|
783
|
+
heartbeatMs: context?.heartbeatMs || 1000,
|
|
784
|
+
onProgress: (event) => _emitGitProgress(onProgress, {
|
|
785
|
+
stage: 'git_worktree_add',
|
|
786
|
+
branch: branchName,
|
|
787
|
+
worktreePath,
|
|
788
|
+
...event,
|
|
789
|
+
}),
|
|
790
|
+
});
|
|
791
|
+
} catch (e) {
|
|
792
|
+
e.stage = 'git_worktree_add';
|
|
793
|
+
e.branch = branchName;
|
|
794
|
+
e.worktreePath = worktreePath;
|
|
795
|
+
if (e.code === 'WORKTREE_CREATE_CANCELLED' || e.cancelled) {
|
|
796
|
+
e.code = 'WORKTREE_CREATE_CANCELLED';
|
|
797
|
+
e.statusCode = 499;
|
|
798
|
+
e.cleanup = await _cleanupPartialWorktree(cwd, e.worktreePath, e.branch, {
|
|
799
|
+
deleteBranch: !!context?.deleteCreatedBranch,
|
|
800
|
+
});
|
|
801
|
+
e.message = `Worktree creation was cancelled while checking out "${e.branch}".`;
|
|
802
|
+
e.message += e.cleanup?.errors?.length
|
|
803
|
+
? ' CTM attempted cleanup, but cleanup was incomplete; run Worktrees > Prune if the branch/path remains.'
|
|
804
|
+
: ' CTM cleaned up the partial checkout; retrying is safe.';
|
|
805
|
+
}
|
|
806
|
+
if (e.code === 'WORKTREE_CREATE_TIMEOUT' || e.timedOut) {
|
|
807
|
+
e.code = 'WORKTREE_CREATE_TIMEOUT';
|
|
808
|
+
e.statusCode = 504;
|
|
809
|
+
e.timeoutMs = timeoutMs;
|
|
810
|
+
e.cleanup = await _cleanupPartialWorktree(cwd, e.worktreePath, e.branch, {
|
|
811
|
+
deleteBranch: !!context?.deleteCreatedBranch,
|
|
812
|
+
});
|
|
813
|
+
e.message = `Worktree creation timed out after ${_formatDuration(timeoutMs)} while checking out "${e.branch}".`;
|
|
814
|
+
e.message += e.cleanup?.errors?.length
|
|
815
|
+
? ' CTM attempted cleanup, but cleanup was incomplete; run Worktrees > Prune if the branch/path remains.'
|
|
816
|
+
: ' CTM cleaned up the partial checkout; retrying is safe.';
|
|
817
|
+
}
|
|
818
|
+
throw e;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function _abortError(message) {
|
|
823
|
+
const err = new Error(message || 'Worktree creation was cancelled.');
|
|
824
|
+
err.code = 'WORKTREE_CREATE_CANCELLED';
|
|
825
|
+
err.cancelled = true;
|
|
826
|
+
err.statusCode = 499;
|
|
827
|
+
return err;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function _throwIfAborted(signal) {
|
|
831
|
+
if (signal && signal.aborted) throw _abortError();
|
|
832
|
+
}
|
|
833
|
+
|
|
308
834
|
// Create or attach to a worktree. The user shouldn't be blocked when a
|
|
309
835
|
// worktree at this name already exists from a prior session — they almost
|
|
310
836
|
// always want to reuse it. Behavior:
|
|
@@ -322,9 +848,21 @@ async function createWorktree(cwd, name, baseBranch, options) {
|
|
|
322
848
|
}
|
|
323
849
|
const base = baseBranch || 'HEAD';
|
|
324
850
|
const namespace = _worktreeNamespace(options);
|
|
851
|
+
const createTimeoutMs = _positiveInt(options?.createTimeoutMs, DEFAULT_WORKTREE_CREATE_TIMEOUT_MS);
|
|
852
|
+
const signal = options?.signal || null;
|
|
853
|
+
const onProgress = typeof options?.onProgress === 'function' ? options.onProgress : null;
|
|
854
|
+
const emitProgress = (event) => _emitGitProgress(onProgress, {
|
|
855
|
+
stage: event?.stage || 'create_worktree',
|
|
856
|
+
status: event?.status || 'running',
|
|
857
|
+
requestedName: name,
|
|
858
|
+
namespace,
|
|
859
|
+
...(event || {}),
|
|
860
|
+
});
|
|
861
|
+
_throwIfAborted(signal);
|
|
325
862
|
const parentDir = _worktreeParentDir(cwd, namespace);
|
|
326
863
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
327
864
|
|
|
865
|
+
emitProgress({ stage: 'scan_existing', message: 'Inspecting existing worktrees and branches.' });
|
|
328
866
|
const worktrees = await listWorktrees(cwd).catch(() => []);
|
|
329
867
|
const _real = (p) => { try { return fs.realpathSync(p); } catch (_) { return p; } };
|
|
330
868
|
|
|
@@ -333,6 +871,13 @@ async function createWorktree(cwd, name, baseBranch, options) {
|
|
|
333
871
|
const initialReal = _real(initialPath);
|
|
334
872
|
const existingAtPath = worktrees.find(w => _real(w.path) === initialReal);
|
|
335
873
|
if (existingAtPath && existingAtPath.branch === name) {
|
|
874
|
+
emitProgress({
|
|
875
|
+
stage: 'reuse_existing',
|
|
876
|
+
status: 'succeeded',
|
|
877
|
+
branch: existingAtPath.branch,
|
|
878
|
+
worktreePath: existingAtPath.path,
|
|
879
|
+
message: `Reusing existing worktree "${name}".`,
|
|
880
|
+
});
|
|
336
881
|
return { path: existingAtPath.path, branch: existingAtPath.branch, reused: true, namespace };
|
|
337
882
|
}
|
|
338
883
|
|
|
@@ -340,12 +885,28 @@ async function createWorktree(cwd, name, baseBranch, options) {
|
|
|
340
885
|
// was removed but its branch was kept.
|
|
341
886
|
if (!existingAtPath && !fs.existsSync(initialPath) && await _branchExists(cwd, name)
|
|
342
887
|
&& !worktrees.some(w => w.branch === name)) {
|
|
343
|
-
|
|
888
|
+
_throwIfAborted(signal);
|
|
889
|
+
await _gitWorktreeAdd(cwd, ['worktree', 'add', initialPath, name], {
|
|
890
|
+
timeoutMs: createTimeoutMs,
|
|
891
|
+
branchName: name,
|
|
892
|
+
worktreePath: initialPath,
|
|
893
|
+
deleteCreatedBranch: false,
|
|
894
|
+
signal,
|
|
895
|
+
onProgress: emitProgress,
|
|
896
|
+
});
|
|
897
|
+
emitProgress({
|
|
898
|
+
stage: 'attach_existing_branch',
|
|
899
|
+
status: 'succeeded',
|
|
900
|
+
branch: name,
|
|
901
|
+
worktreePath: initialPath,
|
|
902
|
+
message: `Attached worktree to existing branch "${name}".`,
|
|
903
|
+
});
|
|
344
904
|
return { path: initialPath, branch: name, attached: true, namespace };
|
|
345
905
|
}
|
|
346
906
|
|
|
347
907
|
// Pick a free (path, branch) pair. Suffix until both slots are clear.
|
|
348
908
|
for (let attempt = 1; attempt <= 20; attempt++) {
|
|
909
|
+
_throwIfAborted(signal);
|
|
349
910
|
const candidateName = attempt === 1 ? name : `${name}-${attempt}`;
|
|
350
911
|
const candidatePath = path.join(parentDir, candidateName);
|
|
351
912
|
const candidateReal = _real(candidatePath);
|
|
@@ -353,7 +914,21 @@ async function createWorktree(cwd, name, baseBranch, options) {
|
|
|
353
914
|
const branchInUse = await _branchExists(cwd, candidateName);
|
|
354
915
|
const pathOnDisk = fs.existsSync(candidatePath);
|
|
355
916
|
if (pathInUse || branchInUse || pathOnDisk) continue;
|
|
356
|
-
await
|
|
917
|
+
await _gitWorktreeAdd(cwd, ['worktree', 'add', '-b', candidateName, candidatePath, base], {
|
|
918
|
+
timeoutMs: createTimeoutMs,
|
|
919
|
+
branchName: candidateName,
|
|
920
|
+
worktreePath: candidatePath,
|
|
921
|
+
deleteCreatedBranch: true,
|
|
922
|
+
signal,
|
|
923
|
+
onProgress: emitProgress,
|
|
924
|
+
});
|
|
925
|
+
emitProgress({
|
|
926
|
+
stage: 'create_branch',
|
|
927
|
+
status: 'succeeded',
|
|
928
|
+
branch: candidateName,
|
|
929
|
+
worktreePath: candidatePath,
|
|
930
|
+
message: `Created worktree "${candidateName}".`,
|
|
931
|
+
});
|
|
357
932
|
return {
|
|
358
933
|
path: candidatePath,
|
|
359
934
|
branch: candidateName,
|
|
@@ -412,6 +987,105 @@ async function mergeWorktree(cwd, branchName, targetBranch, strategy) {
|
|
|
412
987
|
return { merged: true, branch: branchName, into: targetBranch };
|
|
413
988
|
}
|
|
414
989
|
|
|
990
|
+
function _oneLineCommitMessage(message, fallback) {
|
|
991
|
+
const value = String(message || fallback || 'Finish local work')
|
|
992
|
+
.replace(/[\r\n]+/g, ' ')
|
|
993
|
+
.replace(/\s+/g, ' ')
|
|
994
|
+
.trim()
|
|
995
|
+
.slice(0, 180);
|
|
996
|
+
return value || 'Finish local work';
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function commitWorktreeChanges(worktreePath, message) {
|
|
1000
|
+
const status = String(await gitStrict(worktreePath, ['status', '--porcelain=v1', '--untracked-files=all']) || '');
|
|
1001
|
+
if (!status.trim()) {
|
|
1002
|
+
return { committed: false, dirty: false };
|
|
1003
|
+
}
|
|
1004
|
+
await gitStrict(worktreePath, [
|
|
1005
|
+
'add', '-A', '--', '.',
|
|
1006
|
+
':(exclude).claude/worktrees',
|
|
1007
|
+
':(exclude).claude/worktrees/**',
|
|
1008
|
+
':(exclude).walle/worktrees',
|
|
1009
|
+
':(exclude).walle/worktrees/**',
|
|
1010
|
+
]);
|
|
1011
|
+
const stagedExit = await gitExitCode(worktreePath, ['diff', '--cached', '--quiet']);
|
|
1012
|
+
if (stagedExit === 0) {
|
|
1013
|
+
return { committed: false, dirty: true, ignoredOnly: true };
|
|
1014
|
+
}
|
|
1015
|
+
if (stagedExit !== 1) {
|
|
1016
|
+
throw new Error('Could not inspect staged worktree changes before committing.');
|
|
1017
|
+
}
|
|
1018
|
+
const subject = _oneLineCommitMessage(message, 'Finish local work');
|
|
1019
|
+
await gitStrict(worktreePath, ['commit', '-m', subject]);
|
|
1020
|
+
const commit = String(await gitStrict(worktreePath, ['rev-parse', '--short', 'HEAD']) || '').trim();
|
|
1021
|
+
return { committed: true, dirty: true, commit, message: subject };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function _findWorktreeForFinish(cwd, branchName, opts) {
|
|
1025
|
+
const worktreePath = String(opts?.worktreePath || '').trim();
|
|
1026
|
+
const wantedPath = worktreePath ? _realpath(worktreePath) : '';
|
|
1027
|
+
const worktrees = await listWorktrees(cwd);
|
|
1028
|
+
let wt = null;
|
|
1029
|
+
if (wantedPath) {
|
|
1030
|
+
wt = worktrees.find(item => _realpath(item.path) === wantedPath);
|
|
1031
|
+
}
|
|
1032
|
+
if (!wt && branchName) {
|
|
1033
|
+
wt = worktrees.find(item => item.branch === branchName || item.worktreeName === branchName);
|
|
1034
|
+
}
|
|
1035
|
+
if (!wt && branchName === 'main') {
|
|
1036
|
+
wt = worktrees.find(item => item.isMain);
|
|
1037
|
+
}
|
|
1038
|
+
if (!wt) throw new Error(`Worktree not found for branch: ${branchName || worktreePath}`);
|
|
1039
|
+
return wt;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function finishWorktree(cwd, branchName, opts) {
|
|
1043
|
+
opts = opts || {};
|
|
1044
|
+
const targetBranch = opts.targetBranch || 'main';
|
|
1045
|
+
const strategy = opts.strategy;
|
|
1046
|
+
const commitDirty = opts.commitDirty !== false;
|
|
1047
|
+
const wt = await _findWorktreeForFinish(cwd, branchName, opts);
|
|
1048
|
+
const branch = wt.branch || branchName || await getBranch(wt.path);
|
|
1049
|
+
if (!branch || branch === 'HEAD') throw new Error('Cannot finish a detached worktree from the phone. Recover a branch first.');
|
|
1050
|
+
|
|
1051
|
+
let commitResult = { committed: false, dirty: false };
|
|
1052
|
+
const dirtyStatus = String(await gitStrict(wt.path, ['status', '--porcelain=v1', '--untracked-files=all']) || '');
|
|
1053
|
+
if (dirtyStatus.trim()) {
|
|
1054
|
+
if (!commitDirty) {
|
|
1055
|
+
throw new Error('Worktree has uncommitted changes. Commit or stash before merging.');
|
|
1056
|
+
}
|
|
1057
|
+
commitResult = await commitWorktreeChanges(wt.path, opts.commitMessage || `Finish ${branch} work`);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (branch === targetBranch) {
|
|
1061
|
+
return {
|
|
1062
|
+
merged: false,
|
|
1063
|
+
alreadyMain: true,
|
|
1064
|
+
branch,
|
|
1065
|
+
into: targetBranch,
|
|
1066
|
+
worktreePath: wt.path,
|
|
1067
|
+
committed: !!commitResult.committed,
|
|
1068
|
+
commit: commitResult.commit || '',
|
|
1069
|
+
commitMessage: commitResult.message || '',
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const pre = await mergeWorktreePreCheck(cwd, branch, targetBranch);
|
|
1074
|
+
if (pre.conflicts) {
|
|
1075
|
+
const err = new Error('Merge would have conflicts. Resolve manually.');
|
|
1076
|
+
err.code = 'MERGE_CONFLICTS';
|
|
1077
|
+
throw err;
|
|
1078
|
+
}
|
|
1079
|
+
const merged = await mergeWorktree(cwd, branch, targetBranch, strategy);
|
|
1080
|
+
return {
|
|
1081
|
+
...merged,
|
|
1082
|
+
worktreePath: wt.path,
|
|
1083
|
+
committed: !!commitResult.committed,
|
|
1084
|
+
commit: commitResult.commit || '',
|
|
1085
|
+
commitMessage: commitResult.message || '',
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
415
1089
|
// Remove a worktree and optionally delete its branch
|
|
416
1090
|
async function removeWorktree(cwd, worktreePath, deleteBranch) {
|
|
417
1091
|
// Get branch name before removing. Match tolerates /var ↔ /private/var
|
|
@@ -446,6 +1120,15 @@ async function removeWorktree(cwd, worktreePath, deleteBranch) {
|
|
|
446
1120
|
return { removed: true, path: worktreePath, branchDeleted };
|
|
447
1121
|
}
|
|
448
1122
|
|
|
1123
|
+
// Delete a local branch. `-d` refuses to drop unmerged work; `force` uses `-D`.
|
|
1124
|
+
// Caller is responsible for ensuring the branch has no worktree and is not in use
|
|
1125
|
+
// by a live session. Throws on git failure (e.g. unmerged without force).
|
|
1126
|
+
async function deleteBranch(cwd, branchName, force) {
|
|
1127
|
+
const flag = force ? '-D' : '-d';
|
|
1128
|
+
await gitStrict(cwd, ['branch', flag, branchName]);
|
|
1129
|
+
return { deleted: true, branch: branchName, forced: !!force };
|
|
1130
|
+
}
|
|
1131
|
+
|
|
449
1132
|
// Best-effort realpath resolution — falls back to the input if it can't be
|
|
450
1133
|
// resolved (e.g. already deleted, permission issue).
|
|
451
1134
|
function _realpath(p) {
|
|
@@ -459,7 +1142,9 @@ function _realpath(p) {
|
|
|
459
1142
|
// - isCanonical: path lives under an agent-owned <repo>/.claude|.walle/worktrees/<name>
|
|
460
1143
|
// - isGhost: path contains /~/ corruption OR path doesn't exist on disk
|
|
461
1144
|
// - ahead/behind vs mainBranch
|
|
462
|
-
// - dirtyFiles: count of
|
|
1145
|
+
// - dirtyFiles: count of tracked dirty + untracked files
|
|
1146
|
+
// - trackedDirtyFiles/untrackedFiles: split counts so untracked-only
|
|
1147
|
+
// generated artifacts do not block safe sync from main
|
|
463
1148
|
// - unmergedCommits: rev-list count main..HEAD (0 ⇒ safe to delete)
|
|
464
1149
|
// - lastActivity: ISO timestamp of HEAD commit
|
|
465
1150
|
// - summary: 1-line human-readable status
|
|
@@ -476,6 +1161,98 @@ async function _gitSafe(cwd, args, timeoutMs = 5000) {
|
|
|
476
1161
|
});
|
|
477
1162
|
}
|
|
478
1163
|
|
|
1164
|
+
async function _gitSafeRaw(cwd, args, timeoutMs = 5000) {
|
|
1165
|
+
return new Promise((resolve) => {
|
|
1166
|
+
execFile('git', args, { cwd, timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout) => {
|
|
1167
|
+
if (err) return resolve(null);
|
|
1168
|
+
resolve(String(stdout || ''));
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function _parsePorcelainStatusZ(out) {
|
|
1174
|
+
const entries = String(out || '').split('\0').filter(Boolean);
|
|
1175
|
+
const result = {
|
|
1176
|
+
dirtyFiles: 0,
|
|
1177
|
+
trackedDirtyFiles: 0,
|
|
1178
|
+
untrackedFiles: 0,
|
|
1179
|
+
ignoredFiles: 0,
|
|
1180
|
+
unmergedFiles: 0,
|
|
1181
|
+
stagedFiles: 0,
|
|
1182
|
+
unstagedFiles: 0,
|
|
1183
|
+
untrackedPaths: [],
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1187
|
+
const entry = entries[i];
|
|
1188
|
+
if (entry.length < 3) continue;
|
|
1189
|
+
const xy = entry.slice(0, 2);
|
|
1190
|
+
const filePath = entry.slice(3);
|
|
1191
|
+
if (xy === '??') {
|
|
1192
|
+
result.untrackedFiles += 1;
|
|
1193
|
+
result.untrackedPaths.push(filePath);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
if (xy === '!!') {
|
|
1197
|
+
result.ignoredFiles += 1;
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
result.trackedDirtyFiles += 1;
|
|
1202
|
+
if (xy[0] && xy[0] !== ' ') result.stagedFiles += 1;
|
|
1203
|
+
if (xy[1] && xy[1] !== ' ') result.unstagedFiles += 1;
|
|
1204
|
+
if (xy.includes('U') || ['AA', 'DD'].includes(xy)) result.unmergedFiles += 1;
|
|
1205
|
+
// In porcelain v1 -z, rename/copy records include the source path as a
|
|
1206
|
+
// second NUL-terminated field. Skip it so one rename counts once.
|
|
1207
|
+
if (xy[0] === 'R' || xy[0] === 'C') i += 1;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
result.dirtyFiles = result.trackedDirtyFiles + result.untrackedFiles;
|
|
1211
|
+
result.hasOnlyUntrackedFiles = result.untrackedFiles > 0 && result.trackedDirtyFiles === 0;
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function _trackedDirtyCount(wt) {
|
|
1216
|
+
if (!wt) return 0;
|
|
1217
|
+
if (wt.trackedDirtyFiles != null) return Number(wt.trackedDirtyFiles || 0);
|
|
1218
|
+
const dirty = Number(wt.dirtyFiles || 0);
|
|
1219
|
+
const untracked = Number(wt.untrackedFiles || 0);
|
|
1220
|
+
return Math.max(0, dirty - untracked);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function _hasOnlyUntrackedFiles(wt) {
|
|
1224
|
+
if (!wt) return false;
|
|
1225
|
+
const dirty = Number(wt.dirtyFiles || 0);
|
|
1226
|
+
return dirty > 0 && _trackedDirtyCount(wt) === 0;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function _splitNulPaths(out) {
|
|
1230
|
+
return String(out || '').split('\0').filter(Boolean);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function _pathsOverlap(a, b) {
|
|
1234
|
+
const left = String(a || '').replace(/\/+$/, '');
|
|
1235
|
+
const right = String(b || '').replace(/\/+$/, '');
|
|
1236
|
+
if (!left || !right) return false;
|
|
1237
|
+
return left === right || left.startsWith(right + '/') || right.startsWith(left + '/');
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function _untrackedSyncCollisions(worktreePath, targetBranch) {
|
|
1241
|
+
const status = _parsePorcelainStatusZ(await _gitSafeRaw(worktreePath, ['status', '--porcelain=v1', '-uall', '-z']) || '');
|
|
1242
|
+
if (status.trackedDirtyFiles > 0 || status.untrackedPaths.length === 0) {
|
|
1243
|
+
return { status, collisions: [] };
|
|
1244
|
+
}
|
|
1245
|
+
const changedOut = await _gitSafeRaw(worktreePath, ['diff', '--name-only', '-z', 'HEAD', targetBranch], 8000);
|
|
1246
|
+
const changedPaths = _splitNulPaths(changedOut);
|
|
1247
|
+
if (changedPaths.length === 0) return { status, collisions: [] };
|
|
1248
|
+
const collisions = [];
|
|
1249
|
+
for (const untrackedPath of status.untrackedPaths) {
|
|
1250
|
+
const hit = changedPaths.find(changedPath => _pathsOverlap(untrackedPath, changedPath));
|
|
1251
|
+
if (hit) collisions.push({ untrackedPath, incomingPath: hit });
|
|
1252
|
+
}
|
|
1253
|
+
return { status, collisions };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
479
1256
|
function _checkpointRefSlug(branchName) {
|
|
480
1257
|
let slug = String(branchName || 'branch')
|
|
481
1258
|
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
@@ -621,7 +1398,7 @@ function _classifyState(wt) {
|
|
|
621
1398
|
if (wt.isGhost) return 'ghost';
|
|
622
1399
|
if (wt.isMain) return 'primary';
|
|
623
1400
|
if (!wt.branch || wt.branch === 'HEAD') return 'detached';
|
|
624
|
-
if (wt
|
|
1401
|
+
if (_trackedDirtyCount(wt) > 0) return 'dirty';
|
|
625
1402
|
const ahead = wt.ahead || 0;
|
|
626
1403
|
const behind = wt.behind || 0;
|
|
627
1404
|
if (ahead > 0 && behind > 0) return 'diverged';
|
|
@@ -654,16 +1431,20 @@ function _buildSummary(wt) {
|
|
|
654
1431
|
return 'Primary worktree';
|
|
655
1432
|
}
|
|
656
1433
|
if (wt.state === 'detached') return 'Detached HEAD — commits here are at risk. Click Recover branch.';
|
|
1434
|
+
const trackedDirty = _trackedDirtyCount(wt);
|
|
1435
|
+
const untrackedFiles = Number(wt.untrackedFiles || 0);
|
|
657
1436
|
const parts = [];
|
|
658
1437
|
if (wt.ahead > 0) parts.push(`${wt.ahead} ahead`);
|
|
659
1438
|
if (wt.behind > 0) parts.push(`${wt.behind} behind`);
|
|
660
|
-
if (
|
|
1439
|
+
if (trackedDirty > 0) parts.push(`${trackedDirty} dirty`);
|
|
1440
|
+
if (untrackedFiles > 0) parts.push(`${untrackedFiles} untracked`);
|
|
661
1441
|
if (parts.length === 0) return 'Clean — synced with main';
|
|
662
1442
|
let suffix = '';
|
|
663
1443
|
if (wt.state === 'ahead') suffix = ' — ready to merge';
|
|
664
1444
|
else if (wt.state === 'behind') suffix = ' — sync from main';
|
|
665
1445
|
else if (wt.state === 'diverged') suffix = ' — needs sync first';
|
|
666
1446
|
else if (wt.state === 'dirty') suffix = ' — commit or stash';
|
|
1447
|
+
else if (wt.state === 'clean' && untrackedFiles > 0) suffix = ' — untracked files kept local';
|
|
667
1448
|
return parts.join(', ') + suffix;
|
|
668
1449
|
}
|
|
669
1450
|
|
|
@@ -729,10 +1510,12 @@ function _recommendedAction(wt) {
|
|
|
729
1510
|
return { kind: 'primary', label: 'Primary', tone: 'neutral', reason: 'Main checkout.' };
|
|
730
1511
|
}
|
|
731
1512
|
if (!wt.branch || wt.branch === 'HEAD') return { kind: 'recover_branch', label: 'Recover branch', tone: 'danger', reason: 'Detached HEAD commits can become hard to find.' };
|
|
732
|
-
|
|
1513
|
+
const trackedDirty = _trackedDirtyCount(wt);
|
|
1514
|
+
if (trackedDirty > 0) return { kind: 'review_dirty', label: 'Open session', tone: 'warning', reason: `${trackedDirty} tracked dirty file(s).` };
|
|
733
1515
|
if ((wt.ahead || 0) > 0 && (wt.behind || 0) > 0) return { kind: 'sync_branch', label: 'Sync first', tone: 'warning', reason: 'Branch has commits and is behind main.' };
|
|
734
1516
|
if ((wt.behind || 0) > 0) return { kind: 'sync_branch', label: 'Sync', tone: 'warning', reason: 'Branch is behind main.' };
|
|
735
1517
|
if ((wt.ahead || 0) > 0) return { kind: 'finish_work', label: 'Finish', tone: 'success', reason: 'Branch has committed work not on main.' };
|
|
1518
|
+
if (_hasOnlyUntrackedFiles(wt)) return { kind: 'review_dirty', label: 'Open session', tone: 'warning', reason: `${wt.untrackedFiles || wt.dirtyFiles} untracked file(s).` };
|
|
736
1519
|
return { kind: 'cleanup', label: 'Clean up', tone: 'neutral', reason: 'No unmerged commits or dirty files.' };
|
|
737
1520
|
}
|
|
738
1521
|
|
|
@@ -830,6 +1613,37 @@ async function syncWorktree(cwd, branchName, strategy, opts) {
|
|
|
830
1613
|
message: 'Worktree HEAD changed before sync could start.',
|
|
831
1614
|
};
|
|
832
1615
|
}
|
|
1616
|
+
// Make sure main is up to date locally — try a fast-forward fetch but
|
|
1617
|
+
// don't fail if there's no remote.
|
|
1618
|
+
await _gitSafe(repoRoot, ['fetch', 'origin', 'main', '--quiet'], 8000);
|
|
1619
|
+
|
|
1620
|
+
// Pre-check for conflicts using merge-tree (git 2.38+).
|
|
1621
|
+
const preCheck = await mergeWorktreePreCheck(repoRoot, 'main', branchName).catch(() => ({ conflicts: false }));
|
|
1622
|
+
if (preCheck.conflicts) {
|
|
1623
|
+
return { merged: false, conflicts: true, message: 'Merge would conflict — open the worktree in a terminal to resolve.' };
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
const syncSafety = await _untrackedSyncCollisions(wt.path, 'main');
|
|
1627
|
+
if (syncSafety.status.trackedDirtyFiles > 0) {
|
|
1628
|
+
return {
|
|
1629
|
+
merged: false,
|
|
1630
|
+
blocked: true,
|
|
1631
|
+
code: 'TRACKED_DIRTY',
|
|
1632
|
+
beforeHead,
|
|
1633
|
+
message: 'Commit or stash tracked dirty files before syncing from main.',
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
if (syncSafety.collisions.length > 0) {
|
|
1637
|
+
return {
|
|
1638
|
+
merged: false,
|
|
1639
|
+
blocked: true,
|
|
1640
|
+
code: 'UNTRACKED_COLLISION',
|
|
1641
|
+
beforeHead,
|
|
1642
|
+
untrackedCollisions: syncSafety.collisions,
|
|
1643
|
+
message: `Sync would overwrite untracked file(s): ${syncSafety.collisions.map(c => c.untrackedPath).slice(0, 3).join(', ')}`,
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
|
|
833
1647
|
let checkpointRef = '';
|
|
834
1648
|
if (opts.createCheckpoint) {
|
|
835
1649
|
checkpointRef = await _createWorktreeCheckpoint(repoRoot, branchName, beforeHead);
|
|
@@ -844,16 +1658,6 @@ async function syncWorktree(cwd, branchName, strategy, opts) {
|
|
|
844
1658
|
}
|
|
845
1659
|
}
|
|
846
1660
|
|
|
847
|
-
// Make sure main is up to date locally — try a fast-forward fetch but
|
|
848
|
-
// don't fail if there's no remote.
|
|
849
|
-
await _gitSafe(repoRoot, ['fetch', 'origin', 'main', '--quiet'], 8000);
|
|
850
|
-
|
|
851
|
-
// Pre-check for conflicts using merge-tree (git 2.38+).
|
|
852
|
-
const preCheck = await mergeWorktreePreCheck(repoRoot, 'main', branchName).catch(() => ({ conflicts: false }));
|
|
853
|
-
if (preCheck.conflicts) {
|
|
854
|
-
return { merged: false, conflicts: true, message: 'Merge would conflict — open the worktree in a terminal to resolve.' };
|
|
855
|
-
}
|
|
856
|
-
|
|
857
1661
|
const args = strategy === 'rebase' ? ['rebase', 'main'] : ['merge', 'main', '--no-edit'];
|
|
858
1662
|
const out = await _gitSafe(wt.path, args, 30000);
|
|
859
1663
|
if (out === null) {
|
|
@@ -873,7 +1677,8 @@ function _syncAllEligibility(wt) {
|
|
|
873
1677
|
if (!wt || wt.isMain) return { eligible: false, reason: 'primary checkout' };
|
|
874
1678
|
if (wt.isGhost || wt.state === 'ghost') return { eligible: false, reason: 'ghost worktree' };
|
|
875
1679
|
if (!wt.branch || wt.branch === 'HEAD' || wt.state === 'detached') return { eligible: false, reason: 'detached HEAD' };
|
|
876
|
-
|
|
1680
|
+
const trackedDirty = _trackedDirtyCount(wt);
|
|
1681
|
+
if (trackedDirty > 0) return { eligible: false, reason: `${trackedDirty} tracked dirty file(s)` };
|
|
877
1682
|
if ((wt.behind || 0) <= 0) return { eligible: false, reason: 'not behind main' };
|
|
878
1683
|
return { eligible: true, reason: '' };
|
|
879
1684
|
}
|
|
@@ -1058,7 +1863,7 @@ async function listRichWorktrees(cwd, opts) {
|
|
|
1058
1863
|
// Gather status fields in parallel.
|
|
1059
1864
|
const [revlist, statusOut, lastIso, unmergedOut] = await Promise.all([
|
|
1060
1865
|
wt.branch ? _gitSafe(wt.path, ['rev-list', '--left-right', '--count', `${mainBranch}...${wt.branch}`]) : Promise.resolve(null),
|
|
1061
|
-
|
|
1866
|
+
_gitSafeRaw(wt.path, ['status', '--porcelain=v1', '-uall', '-z']),
|
|
1062
1867
|
_gitSafe(wt.path, ['log', '-1', '--format=%cI', 'HEAD']),
|
|
1063
1868
|
wt.branch && !wt.isMain ? _gitSafe(wt.path, ['rev-list', '--count', `${mainBranch}..HEAD`]) : Promise.resolve('0'),
|
|
1064
1869
|
]);
|
|
@@ -1071,11 +1876,18 @@ async function listRichWorktrees(cwd, opts) {
|
|
|
1071
1876
|
ahead = parseInt(parts[1], 10) || 0;
|
|
1072
1877
|
}
|
|
1073
1878
|
}
|
|
1074
|
-
const
|
|
1879
|
+
const status = _parsePorcelainStatusZ(statusOut || '');
|
|
1880
|
+
const dirtyFiles = status.dirtyFiles;
|
|
1075
1881
|
const unmergedCommits = parseInt(unmergedOut, 10) || 0;
|
|
1076
1882
|
|
|
1077
1883
|
const out = {
|
|
1078
1884
|
...wt, isGhost: false, isCanonical, ahead, behind, dirtyFiles, unmergedCommits,
|
|
1885
|
+
trackedDirtyFiles: status.trackedDirtyFiles,
|
|
1886
|
+
untrackedFiles: status.untrackedFiles,
|
|
1887
|
+
unmergedFiles: status.unmergedFiles,
|
|
1888
|
+
stagedFiles: status.stagedFiles,
|
|
1889
|
+
unstagedFiles: status.unstagedFiles,
|
|
1890
|
+
hasOnlyUntrackedFiles: status.hasOnlyUntrackedFiles,
|
|
1079
1891
|
lastActivity: lastIso || null,
|
|
1080
1892
|
lastActivityRel: _formatRelativeTime(lastIso),
|
|
1081
1893
|
mainBranch,
|
|
@@ -1104,6 +1916,62 @@ async function listRichWorktrees(cwd, opts) {
|
|
|
1104
1916
|
return enriched;
|
|
1105
1917
|
}
|
|
1106
1918
|
|
|
1919
|
+
// Lean single-worktree status for ONE path — the fast counterpart to
|
|
1920
|
+
// listRichWorktrees(). Used by the on-turn-finish refresh so the sidebar
|
|
1921
|
+
// dirty/commit badge updates in ~seconds instead of waiting on the 30s
|
|
1922
|
+
// listRichWorktrees() fan-out (a documented CPU hog: ~4 git spawns PER worktree
|
|
1923
|
+
// across ~40 worktrees). This does those ~4 git calls for just the one worktree.
|
|
1924
|
+
// Computes only the fields the session worktree-status badge needs (live branch,
|
|
1925
|
+
// ahead/behind vs main, dirty counts, unmerged commits, state). Best-effort:
|
|
1926
|
+
// returns null if the path is missing or not a git worktree. mainRemote is a
|
|
1927
|
+
// stub here (only meaningful for the primary worktree, which never shows the
|
|
1928
|
+
// attention badge); callers needing remote state use listRichWorktrees().
|
|
1929
|
+
async function richWorktreeStatusForPath(worktreePath, opts = {}) {
|
|
1930
|
+
const mainBranch = opts.mainBranch || 'main';
|
|
1931
|
+
if (!worktreePath || _isGhostPath(worktreePath)) return null;
|
|
1932
|
+
const branch = await _gitSafe(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
1933
|
+
if (!branch) return null;
|
|
1934
|
+
const detached = branch === 'HEAD';
|
|
1935
|
+
const isMain = !detached && (branch === mainBranch || branch === 'main' || branch === 'master');
|
|
1936
|
+
const [revlist, statusOut, lastIso, unmergedOut] = await Promise.all([
|
|
1937
|
+
!detached ? _gitSafe(worktreePath, ['rev-list', '--left-right', '--count', `${mainBranch}...${branch}`]) : Promise.resolve(null),
|
|
1938
|
+
_gitSafeRaw(worktreePath, ['status', '--porcelain=v1', '-uall', '-z']),
|
|
1939
|
+
_gitSafe(worktreePath, ['log', '-1', '--format=%cI', 'HEAD']),
|
|
1940
|
+
(!isMain && !detached) ? _gitSafe(worktreePath, ['rev-list', '--count', `${mainBranch}..HEAD`]) : Promise.resolve('0'),
|
|
1941
|
+
]);
|
|
1942
|
+
let ahead = 0, behind = 0;
|
|
1943
|
+
if (revlist) {
|
|
1944
|
+
const parts = revlist.split(/\s+/).filter(Boolean);
|
|
1945
|
+
if (parts.length >= 2) { behind = parseInt(parts[0], 10) || 0; ahead = parseInt(parts[1], 10) || 0; }
|
|
1946
|
+
}
|
|
1947
|
+
const status = _parsePorcelainStatusZ(statusOut || '');
|
|
1948
|
+
const out = {
|
|
1949
|
+
path: worktreePath,
|
|
1950
|
+
worktreeName: path.basename(worktreePath || ''),
|
|
1951
|
+
branch: detached ? '' : branch,
|
|
1952
|
+
head: null,
|
|
1953
|
+
isMain,
|
|
1954
|
+
isGhost: false,
|
|
1955
|
+
isCanonical: true,
|
|
1956
|
+
ahead, behind,
|
|
1957
|
+
dirtyFiles: status.dirtyFiles,
|
|
1958
|
+
trackedDirtyFiles: status.trackedDirtyFiles,
|
|
1959
|
+
untrackedFiles: status.untrackedFiles,
|
|
1960
|
+
unmergedFiles: status.unmergedFiles,
|
|
1961
|
+
stagedFiles: status.stagedFiles,
|
|
1962
|
+
unstagedFiles: status.unstagedFiles,
|
|
1963
|
+
hasOnlyUntrackedFiles: status.hasOnlyUntrackedFiles,
|
|
1964
|
+
unmergedCommits: parseInt(unmergedOut, 10) || 0,
|
|
1965
|
+
lastActivity: lastIso || null,
|
|
1966
|
+
lastActivityRel: _formatRelativeTime(lastIso),
|
|
1967
|
+
mainBranch,
|
|
1968
|
+
mainRemote: { branch: mainBranch, remote: null, ahead: 0, behind: 0, state: 'unknown' },
|
|
1969
|
+
};
|
|
1970
|
+
out.state = _classifyState(out);
|
|
1971
|
+
out.summary = _buildSummary(out);
|
|
1972
|
+
return out;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1107
1975
|
// Get the list of unmerged commits between main and a worktree branch
|
|
1108
1976
|
// (used by safe-delete and PR-body generation).
|
|
1109
1977
|
async function getUnmergedCommitList(cwd, branchName, baseBranch) {
|
|
@@ -1175,13 +2043,15 @@ async function pushAndCreatePR(cwd, branchName, opts) {
|
|
|
1175
2043
|
|
|
1176
2044
|
module.exports = {
|
|
1177
2045
|
git, getCommits, getCommitLog, getBranch, getDiffStat, getFullDiff, getStagedDiff, parseDiff,
|
|
1178
|
-
|
|
2046
|
+
resolveMainBranch, getDefaultReviewBase, getDiffVsRef, getDiffStatVsRef, getDefaultDiffStat, getFileAtRef,
|
|
2047
|
+
listWorktrees, createWorktree, mergeWorktreePreCheck, mergeWorktree, commitWorktreeChanges, finishWorktree, removeWorktree, deleteBranch,
|
|
1179
2048
|
// Rich-status + new operations
|
|
1180
|
-
listRichWorktrees, syncWorktree, syncAllWorktrees, pruneGhosts, recoverDetachedHead, recoverWorktree,
|
|
2049
|
+
listRichWorktrees, richWorktreeStatusForPath, syncWorktree, syncAllWorktrees, pruneGhosts, recoverDetachedHead, recoverWorktree,
|
|
1181
2050
|
getUnmergedCommitList, pushAndCreatePR,
|
|
1182
2051
|
// Internal helpers exposed for tests
|
|
1183
2052
|
_classifyState, _buildSummary, _isGhostPath, _isCanonicalPath, STATE_SORT_RANK,
|
|
1184
2053
|
_classifyMainRemote, _recommendedAction, _syncAllEligibility, _sameWorktreePath,
|
|
1185
2054
|
_parseGitHubRemoteUrl, _githubRemoteSpec, _buildGhPrCreateArgs,
|
|
1186
2055
|
_worktreeNamespace, _worktreeParentDir, _checkpointRefName, _checkpointRefSlug,
|
|
2056
|
+
_invalidateGitFactCache,
|
|
1187
2057
|
};
|