@duckmind/dm-windows-x64 0.61.4 → 0.61.9
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +211 -67
- package/extensions/dm-subagents/agents/claude-code-writer.md +15 -0
- package/extensions/dm-subagents/agents/claude-code.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec-writer.md +15 -0
- package/extensions/dm-subagents/agents/codex-exec.md +15 -0
- package/extensions/dm-subagents/agents/cursor-agent-writer.md +14 -0
- package/extensions/dm-subagents/agents/cursor-agent.md +14 -0
- package/extensions/dm-subagents/agents/delegate.md +3 -2
- package/extensions/dm-subagents/agents/oracle.md +10 -5
- package/extensions/dm-subagents/agents/researcher.md +2 -2
- package/extensions/dm-subagents/agents/reviewer.md +17 -7
- package/extensions/dm-subagents/agents/scout.md +5 -5
- package/extensions/dm-subagents/agents/worker.md +6 -2
- package/extensions/dm-subagents/async-retention-discovery-worker.mjs +167 -0
- package/extensions/dm-subagents/index.js +4 -0
- package/extensions/dm-subagents/inspector-runner.mjs +10 -0
- package/extensions/dm-subagents/install.mjs +3 -2
- package/extensions/dm-subagents/package.json +2 -2
- package/extensions/dm-subagents/prompts/council.md +60 -0
- package/extensions/dm-subagents/prompts/parallel-review.md +5 -1
- package/extensions/dm-subagents/prompts/review-loop.md +13 -7
- package/extensions/dm-subagents/skills/council-mode/SKILL.md +59 -0
- package/extensions/dm-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/extensions/dm-subagents/skills/dm-subagents/SKILL.md +96 -911
- package/extensions/dm-subagents/skills/dm-subagents/references/constraints-and-recipes.md +70 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/execution-controls.md +539 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/management-authoring-rpc.md +161 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/multi-lane-orchestration.md +51 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/prompting-and-roles.md +295 -0
- package/extensions/dm-subagents/skills/dm-subagents/references/review-and-validation.md +73 -0
- package/extensions/dm-subagents/src/agents/agent-management.js +716 -484
- package/extensions/dm-subagents/src/agents/agent-refinements.js +563 -0
- package/extensions/dm-subagents/src/agents/agent-serializer.js +70 -5
- package/extensions/dm-subagents/src/agents/agents.js +1447 -299
- package/extensions/dm-subagents/src/agents/builtin-names.js +15 -0
- package/extensions/dm-subagents/src/agents/chain-serializer.js +12 -7
- package/extensions/dm-subagents/src/agents/frontmatter.js +64 -14
- package/extensions/dm-subagents/src/agents/identity.js +1 -1
- package/extensions/dm-subagents/src/agents/proactive-skills.js +14 -11
- package/extensions/dm-subagents/src/agents/runtime-agent-events.js +49 -0
- package/extensions/dm-subagents/src/agents/runtime-agent-registry.js +412 -0
- package/extensions/dm-subagents/src/agents/skills.js +52 -35
- package/extensions/dm-subagents/src/api/agents.js +6 -0
- package/extensions/dm-subagents/src/api/background-work.js +151 -0
- package/extensions/dm-subagents/src/api/capability-ceiling.js +12 -0
- package/extensions/dm-subagents/src/api/control-channel.js +3 -0
- package/extensions/dm-subagents/src/api/delegation.js +5 -0
- package/extensions/dm-subagents/src/api/dm-args.js +3 -0
- package/extensions/dm-subagents/src/api/external-job-provider.js +137 -0
- package/extensions/dm-subagents/src/api/external-runs.js +233 -0
- package/extensions/dm-subagents/src/api/intercom-bridge.js +3 -0
- package/extensions/dm-subagents/src/api/preflight.js +322 -0
- package/extensions/dm-subagents/src/api/project-panes.js +11 -0
- package/extensions/dm-subagents/src/api/shared-types.js +4 -0
- package/extensions/dm-subagents/src/extension/config.js +175 -0
- package/extensions/dm-subagents/src/extension/control-notices.js +4 -43
- package/extensions/dm-subagents/src/extension/doctor.js +71 -17
- package/extensions/dm-subagents/src/extension/fanout-child.js +49 -32
- package/extensions/dm-subagents/src/extension/index.js +711 -265
- package/extensions/dm-subagents/src/extension/public-execution.js +114 -0
- package/extensions/dm-subagents/src/extension/rpc.js +427 -22
- package/extensions/dm-subagents/src/extension/schemas.js +158 -65
- package/extensions/dm-subagents/src/extension/steering-notices.js +23 -0
- package/extensions/dm-subagents/src/extension/subagent-guide.js +31 -0
- package/extensions/dm-subagents/src/extension/tool-description.js +92 -74
- package/extensions/dm-subagents/src/extension/tool-result.js +7 -0
- package/extensions/dm-subagents/src/inspectors/herdr/actions.js +218 -0
- package/extensions/dm-subagents/src/inspectors/herdr/client.js +123 -0
- package/extensions/dm-subagents/src/inspectors/herdr/focus.js +47 -0
- package/extensions/dm-subagents/src/inspectors/herdr/inspector-runner.js +160 -0
- package/extensions/dm-subagents/src/inspectors/herdr/project-panes.js +618 -0
- package/extensions/dm-subagents/src/inspectors/herdr/session-roots-codec.js +21 -0
- package/extensions/dm-subagents/src/inspectors/herdr/shell-command.js +15 -0
- package/extensions/dm-subagents/src/integrations/herdr-status.js +377 -0
- package/extensions/dm-subagents/src/intercom/intercom-bridge.js +17 -13
- package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +371 -79
- package/extensions/dm-subagents/src/intercom/result-intercom.js +47 -7
- package/extensions/dm-subagents/src/missions/actions.js +394 -0
- package/extensions/dm-subagents/src/missions/goal-driver.js +149 -0
- package/extensions/dm-subagents/src/missions/lifecycle.js +331 -0
- package/extensions/dm-subagents/src/missions/store.js +548 -0
- package/extensions/dm-subagents/src/missions/types.js +9 -0
- package/extensions/dm-subagents/src/missions/workflow-state.js +245 -0
- package/extensions/dm-subagents/src/policy/authority.js +37 -0
- package/extensions/dm-subagents/src/profiles/profiles.js +36 -18
- package/extensions/dm-subagents/src/runs/background/active-async-capacity.js +427 -0
- package/extensions/dm-subagents/src/runs/background/active-run-index.js +122 -0
- package/extensions/dm-subagents/src/runs/background/async-execution.js +925 -137
- package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +515 -148
- package/extensions/dm-subagents/src/runs/background/async-resume.js +378 -51
- package/extensions/dm-subagents/src/runs/background/async-retention.js +828 -0
- package/extensions/dm-subagents/src/runs/background/async-status-snapshot.js +31 -0
- package/extensions/dm-subagents/src/runs/background/async-status.js +259 -22
- package/extensions/dm-subagents/src/runs/background/auto-drain.js +46 -0
- package/extensions/dm-subagents/src/runs/background/chain-append.js +50 -15
- package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +67 -12
- package/extensions/dm-subagents/src/runs/background/completion-batcher.js +5 -1
- package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +3 -11
- package/extensions/dm-subagents/src/runs/background/completion-replay.js +245 -0
- package/extensions/dm-subagents/src/runs/background/control-channel.js +423 -36
- package/extensions/dm-subagents/src/runs/background/fleet-view.js +132 -59
- package/extensions/dm-subagents/src/runs/background/index-segment.js +38 -0
- package/extensions/dm-subagents/src/runs/background/inspect-rpc.js +373 -0
- package/extensions/dm-subagents/src/runs/background/notify.js +357 -57
- package/extensions/dm-subagents/src/runs/background/owned-process-tree.js +86 -0
- package/extensions/dm-subagents/src/runs/background/process-terminal.js +269 -0
- package/extensions/dm-subagents/src/runs/background/result-delivery-ownership.js +34 -0
- package/extensions/dm-subagents/src/runs/background/result-files.js +469 -0
- package/extensions/dm-subagents/src/runs/background/result-watcher.js +540 -75
- package/extensions/dm-subagents/src/runs/background/resume-guidance.js +44 -0
- package/extensions/dm-subagents/src/runs/background/retained-children.js +119 -0
- package/extensions/dm-subagents/src/runs/background/run-id-query.js +5 -0
- package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +93 -9
- package/extensions/dm-subagents/src/runs/background/run-status.js +303 -32
- package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +784 -376
- package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +75 -34
- package/extensions/dm-subagents/src/runs/background/steering.js +221 -0
- package/extensions/dm-subagents/src/runs/background/subagent-runner.js +3106 -844
- package/extensions/dm-subagents/src/runs/background/subagent-wait.js +529 -0
- package/extensions/dm-subagents/src/runs/background/terminal-run-index.js +106 -0
- package/extensions/dm-subagents/src/runs/background/top-level-async.js +1 -1
- package/extensions/dm-subagents/src/runs/background/wait-completions.js +155 -0
- package/extensions/dm-subagents/src/runs/background/wait-config.js +46 -0
- package/extensions/dm-subagents/src/runs/background/wait-subscriptions.js +278 -0
- package/extensions/dm-subagents/src/runs/background/wait-tool.js +47 -0
- package/extensions/dm-subagents/src/runs/foreground/async-dismiss-action.js +81 -0
- package/extensions/dm-subagents/src/runs/foreground/async-steering-action.js +245 -0
- package/extensions/dm-subagents/src/runs/foreground/async-stop-action.js +74 -0
- package/extensions/dm-subagents/src/runs/foreground/execution.js +1401 -342
- package/extensions/dm-subagents/src/runs/foreground/foreground-control.js +133 -0
- package/extensions/dm-subagents/src/runs/foreground/foreground-history.js +148 -0
- package/extensions/dm-subagents/src/runs/foreground/prompt-audit.js +139 -0
- package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +4278 -1441
- package/extensions/dm-subagents/src/runs/foreground/workflow-detach-reconcile.js +278 -0
- package/extensions/dm-subagents/src/runs/foreground/workflow-foreground-steering.js +155 -0
- package/extensions/dm-subagents/src/runs/shared/abort-recovery.js +97 -0
- package/extensions/dm-subagents/src/runs/shared/acceptance.js +597 -148
- package/extensions/dm-subagents/src/runs/shared/agent-contract.js +35 -0
- package/extensions/dm-subagents/src/runs/shared/async-status-projection.js +472 -0
- package/extensions/dm-subagents/src/runs/shared/background-process-options.js +6 -0
- package/extensions/dm-subagents/src/runs/shared/capability-ceiling.js +175 -0
- package/extensions/dm-subagents/src/runs/shared/child-identity.js +32 -0
- package/extensions/dm-subagents/src/runs/shared/child-launch-plan.js +65 -0
- package/extensions/dm-subagents/src/runs/shared/child-protocol.js +447 -0
- package/extensions/dm-subagents/src/runs/shared/claude-code-adapter.js +120 -0
- package/extensions/dm-subagents/src/runs/shared/codex-exec-adapter.js +129 -0
- package/extensions/dm-subagents/src/runs/shared/completion-evidence.js +40 -0
- package/extensions/dm-subagents/src/runs/shared/completion-guard.js +140 -83
- package/extensions/dm-subagents/src/runs/shared/context-mode.js +38 -0
- package/extensions/dm-subagents/src/runs/shared/cursor-agent-adapter.js +101 -0
- package/extensions/dm-subagents/src/runs/shared/dm-args.js +445 -72
- package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +27 -16
- package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +19 -6
- package/extensions/dm-subagents/src/runs/shared/extension-bindings.js +81 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-contract.js +134 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-preflight.js +98 -0
- package/extensions/dm-subagents/src/runs/shared/external-cli-runner.js +419 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-bridge.js +404 -0
- package/extensions/dm-subagents/src/runs/shared/external-job-runner.js +334 -0
- package/extensions/dm-subagents/src/runs/shared/fast-mode-extension.js +8 -0
- package/extensions/dm-subagents/src/runs/shared/host-step-status.js +228 -0
- package/extensions/dm-subagents/src/runs/shared/lane-metadata.js +104 -0
- package/extensions/dm-subagents/src/runs/shared/launch-cwd.js +17 -0
- package/extensions/dm-subagents/src/runs/shared/llm-intent-arbiter.js +190 -0
- package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +48 -3
- package/extensions/dm-subagents/src/runs/shared/mcp-config-sources.js +387 -0
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +212 -137
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-grant.js +131 -0
- package/extensions/dm-subagents/src/runs/shared/model-exclusions.js +207 -0
- package/extensions/dm-subagents/src/runs/shared/model-fallback.js +225 -55
- package/extensions/dm-subagents/src/runs/shared/model-scope.js +85 -28
- package/extensions/dm-subagents/src/runs/shared/mutation-evidence.js +182 -0
- package/extensions/dm-subagents/src/runs/shared/nested-events.js +264 -102
- package/extensions/dm-subagents/src/runs/shared/nested-render.js +15 -5
- package/extensions/dm-subagents/src/runs/shared/orca-progress-tabs.js +505 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-handoff.js +653 -0
- package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +29 -12
- package/extensions/dm-subagents/src/runs/shared/permissions.js +108 -0
- package/extensions/dm-subagents/src/runs/shared/process-signal.js +13 -0
- package/extensions/dm-subagents/src/runs/shared/run-fanout-budget.js +257 -0
- package/extensions/dm-subagents/src/runs/shared/run-history.js +133 -13
- package/extensions/dm-subagents/src/runs/shared/runtime-acknowledged-extensions.js +62 -0
- package/extensions/dm-subagents/src/runs/shared/session-lease.js +225 -0
- package/extensions/dm-subagents/src/runs/shared/single-output.js +129 -27
- package/extensions/dm-subagents/src/runs/shared/spawn-budget.js +95 -0
- package/extensions/dm-subagents/src/runs/shared/structured-output.js +129 -10
- package/extensions/dm-subagents/src/runs/shared/subagent-control.js +66 -11
- package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +548 -70
- package/extensions/dm-subagents/src/runs/shared/subagent-startup-retry.js +50 -0
- package/extensions/dm-subagents/src/runs/shared/task-intent.js +130 -0
- package/extensions/dm-subagents/src/runs/shared/tool-availability.js +59 -0
- package/extensions/dm-subagents/src/runs/shared/tool-budget.js +7 -5
- package/extensions/dm-subagents/src/runs/shared/tool-timeout.js +64 -0
- package/extensions/dm-subagents/src/runs/shared/usage-budget.js +74 -0
- package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +22 -0
- package/extensions/dm-subagents/src/runs/shared/worktree-cleanup-plan.js +721 -0
- package/extensions/dm-subagents/src/runs/shared/worktree.js +168 -19
- package/extensions/dm-subagents/src/shared/accessible-dir.js +35 -0
- package/extensions/dm-subagents/src/shared/agent-stream-options.js +3 -0
- package/extensions/dm-subagents/src/shared/artifacts.js +170 -11
- package/extensions/dm-subagents/src/shared/atomic-json.js +36 -38
- package/extensions/dm-subagents/src/shared/capacity-resilient-json.js +77 -0
- package/extensions/dm-subagents/src/shared/child-session-name.js +15 -0
- package/extensions/dm-subagents/src/shared/child-transcript.js +57 -2
- package/extensions/dm-subagents/src/shared/completion-owner.js +7 -0
- package/extensions/dm-subagents/src/shared/display-text.js +142 -0
- package/extensions/dm-subagents/src/shared/extension-context.js +17 -0
- package/extensions/dm-subagents/src/shared/file-coalescer.js +9 -0
- package/extensions/dm-subagents/src/shared/file-system-retry.js +56 -0
- package/extensions/dm-subagents/src/shared/fork-context.js +96 -33
- package/extensions/dm-subagents/src/shared/formatters.js +21 -7
- package/extensions/dm-subagents/src/shared/launch-contract.js +94 -0
- package/extensions/dm-subagents/src/shared/model-info.js +10 -5
- package/extensions/dm-subagents/src/shared/node-executable.js +19 -0
- package/extensions/dm-subagents/src/shared/prompt-resources.js +10 -0
- package/extensions/dm-subagents/src/shared/pruned-fork.js +427 -0
- package/extensions/dm-subagents/src/shared/session-file-trust.js +19 -0
- package/extensions/dm-subagents/src/shared/session-tokens.js +14 -3
- package/extensions/dm-subagents/src/shared/settings.js +39 -47
- package/extensions/dm-subagents/src/shared/shortcuts.js +16 -0
- package/extensions/dm-subagents/src/shared/status-format.js +9 -2
- package/extensions/dm-subagents/src/shared/thinking-ceiling.js +41 -0
- package/extensions/dm-subagents/src/shared/types.js +47 -7
- package/extensions/dm-subagents/src/shared/utf8.js +12 -0
- package/extensions/dm-subagents/src/shared/utils.js +151 -131
- package/extensions/dm-subagents/src/shared/watch-strategy.js +3 -0
- package/extensions/dm-subagents/src/shared/workflow-child-permit.js +84 -0
- package/extensions/dm-subagents/src/slash/delegation-adapters.js +274 -0
- package/extensions/dm-subagents/src/slash/delegation-json.js +113 -0
- package/extensions/dm-subagents/src/slash/delegation-request.js +152 -0
- package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +294 -243
- package/extensions/dm-subagents/src/slash/prompt-workflows.js +35 -73
- package/extensions/dm-subagents/src/slash/selector.js +101 -0
- package/extensions/dm-subagents/src/slash/slash-bridge.js +17 -1
- package/extensions/dm-subagents/src/slash/slash-commands.js +722 -732
- package/extensions/dm-subagents/src/slash/slash-live-state.js +37 -19
- package/extensions/dm-subagents/src/slash/subagents-admin.js +410 -0
- package/extensions/dm-subagents/src/tui/fleet-status.js +824 -0
- package/extensions/dm-subagents/src/tui/fleet-transcript.js +479 -0
- package/extensions/dm-subagents/src/tui/fleet.js +1326 -0
- package/extensions/dm-subagents/src/tui/render-helpers.js +22 -0
- package/extensions/dm-subagents/src/tui/render.js +1511 -261
- package/extensions/dm-subagents/src/watchdog/change-signature.js +220 -0
- package/extensions/dm-subagents/src/watchdog/child-status.js +151 -0
- package/extensions/dm-subagents/src/watchdog/emission-guard.js +90 -0
- package/extensions/dm-subagents/src/watchdog/lsp-diagnostics.js +484 -0
- package/extensions/dm-subagents/src/watchdog/model-selection.js +154 -0
- package/extensions/dm-subagents/src/watchdog/permission-arbiter.js +138 -0
- package/extensions/dm-subagents/src/watchdog/register-child.js +112 -0
- package/extensions/dm-subagents/src/watchdog/register-main.js +419 -0
- package/extensions/dm-subagents/src/watchdog/render.js +54 -0
- package/extensions/dm-subagents/src/watchdog/review.js +251 -0
- package/extensions/dm-subagents/src/watchdog/runtime.js +803 -0
- package/extensions/dm-subagents/src/watchdog/scope.js +56 -0
- package/extensions/dm-subagents/src/watchdog/settings.js +515 -0
- package/extensions/dm-subagents/src/watchdog/tool-actions.js +151 -0
- package/extensions/dm-subagents/src/watchdog/turn-delta.js +169 -0
- package/extensions/dm-subagents/src/watchdog/types.js +29 -0
- package/extensions/dm-subagents/src/watchdog/warning-format.js +58 -0
- package/extensions/dm-subagents/src/workflows/chat-progress.js +116 -0
- package/extensions/dm-subagents/src/workflows/host-command.js +227 -0
- package/extensions/dm-subagents/src/workflows/scripted-workflow.js +2011 -0
- package/extensions/dm-subagents/src/workflows/workflow-child-summary.js +116 -0
- package/extensions/dm-subagents/src/workflows/workflow-preflight.js +243 -0
- package/extensions/dm-subagents/src/workflows/workflow-receipt.js +387 -0
- package/extensions/dm-subagents/src/workflows/workflow-settlement.js +189 -0
- package/package.json +4 -3
- package/extensions/dm-fff/package.json +0 -21
- package/extensions/dm-fff/src/index.js +0 -691
- package/extensions/dm-fff/src/query.js +0 -60
- package/extensions/dm-subagents/agents/context-builder.md +0 -46
- package/extensions/dm-subagents/agents/planner.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-context-build.md +0 -55
- package/extensions/dm-subagents/prompts/parallel-handoff-plan.md +0 -61
- package/extensions/dm-subagents/src/runs/background/wait.js +0 -206
- package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +0 -1013
- package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +0 -981
- package/extensions/dm-subagents/src/runs/shared/turn-budget.js +0 -50
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { writeAtomicJson } from "../../shared/atomic-json.js";
|
|
7
|
+
import { PROJECT_SUBAGENTS_RELATIVE_DIR } from "../../shared/artifacts.js";
|
|
8
|
+
import { DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS, activeRunMarkerAgeMs } from "../background/active-run-index.js";
|
|
9
|
+
import { isTerminalParallelHandoffChildStatus } from "./parallel-handoff.js";
|
|
10
|
+
export const WORKTREE_CLEANUP_PLAN_VERSION = 1;
|
|
11
|
+
export const WORKTREE_CLEANUP_PLAN_TTL_MS = 30 * 60 * 1000;
|
|
12
|
+
const MAX_DISCOVERED_HANDOFFS = 256;
|
|
13
|
+
const MAX_PLAN_ENTRIES = 512;
|
|
14
|
+
const MAX_METADATA_FILE_BYTES = 2 * 1024 * 1024;
|
|
15
|
+
function runGit(cwd, args) {
|
|
16
|
+
try {
|
|
17
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", windowsHide: true });
|
|
18
|
+
return {
|
|
19
|
+
stdout: result.stdout ?? "",
|
|
20
|
+
stderr: result.stderr ?? "",
|
|
21
|
+
status: result.status,
|
|
22
|
+
...result.error ? { error: result.error } : {}
|
|
23
|
+
};
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
stdout: "",
|
|
27
|
+
stderr: "",
|
|
28
|
+
status: null,
|
|
29
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function gitFailure(result, command) {
|
|
34
|
+
return result.error?.message || result.stderr.trim() || result.stdout.trim() || `${command} failed`;
|
|
35
|
+
}
|
|
36
|
+
function runGitChecked(cwd, args) {
|
|
37
|
+
const result = runGit(cwd, args);
|
|
38
|
+
if (result.status !== 0)
|
|
39
|
+
throw new Error(gitFailure(result, `git -C ${cwd} ${args.join(" ")}`));
|
|
40
|
+
return result.stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
function realpathExisting(candidate, nativeRealpath = fs.realpathSync.native, fallbackRealpath = fs.realpathSync) {
|
|
43
|
+
try {
|
|
44
|
+
return nativeRealpath(candidate);
|
|
45
|
+
} catch {
|
|
46
|
+
return fallbackRealpath(candidate);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const __testables = { realpathExisting };
|
|
50
|
+
function comparablePath(candidate) {
|
|
51
|
+
let current = path.resolve(candidate);
|
|
52
|
+
const missingSegments = [];
|
|
53
|
+
let canonical = current;
|
|
54
|
+
while (true) {
|
|
55
|
+
try {
|
|
56
|
+
canonical = path.join(realpathExisting(current), ...missingSegments.reverse());
|
|
57
|
+
break;
|
|
58
|
+
} catch {
|
|
59
|
+
const parent = path.dirname(current);
|
|
60
|
+
if (parent === current)
|
|
61
|
+
break;
|
|
62
|
+
missingSegments.push(path.basename(current));
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const normalized = path.normalize(canonical);
|
|
67
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
68
|
+
}
|
|
69
|
+
function samePath(left, right) {
|
|
70
|
+
if (comparablePath(left) === comparablePath(right))
|
|
71
|
+
return true;
|
|
72
|
+
if (process.platform !== "win32")
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
const leftStat = fs.statSync(left);
|
|
76
|
+
const rightStat = fs.statSync(right);
|
|
77
|
+
return leftStat.dev !== 0 && leftStat.ino !== 0 && leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function resolveExistingPath(candidate) {
|
|
83
|
+
try {
|
|
84
|
+
return realpathExisting(candidate);
|
|
85
|
+
} catch {
|
|
86
|
+
return path.resolve(candidate);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function pathInside(root, candidate, strict = false) {
|
|
90
|
+
const normalizedRoot = comparablePath(root);
|
|
91
|
+
const normalizedCandidate = comparablePath(candidate);
|
|
92
|
+
if (!strict && normalizedRoot === normalizedCandidate)
|
|
93
|
+
return true;
|
|
94
|
+
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
95
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
96
|
+
}
|
|
97
|
+
function sha256(value) {
|
|
98
|
+
return createHash("sha256").update(value, "utf-8").digest("hex");
|
|
99
|
+
}
|
|
100
|
+
function validatePlanId(planId) {
|
|
101
|
+
if (!/^[A-Za-z0-9._-]+$/.test(planId) || planId === "." || planId === "..") {
|
|
102
|
+
throw new Error("worktree cleanup plan id must contain only letters, numbers, dots, underscores, or hyphens");
|
|
103
|
+
}
|
|
104
|
+
return planId;
|
|
105
|
+
}
|
|
106
|
+
function resolveRepoRoot(repo) {
|
|
107
|
+
const requested = path.resolve(repo);
|
|
108
|
+
const toplevel = runGitChecked(requested, ["rev-parse", "--show-toplevel"]);
|
|
109
|
+
return realpathExisting(toplevel);
|
|
110
|
+
}
|
|
111
|
+
function resolveCleanupBaseDir(repoRoot, configuredBaseDir) {
|
|
112
|
+
const raw = configuredBaseDir ?? process.env.DM_SUBAGENTS_WORKTREE_DIR ?? os.tmpdir();
|
|
113
|
+
const trimmed = raw.trim();
|
|
114
|
+
if (!trimmed)
|
|
115
|
+
throw new Error("worktree base directory cannot be empty");
|
|
116
|
+
const expanded = trimmed.startsWith("~/") ? path.join(os.homedir(), trimmed.slice(2)) : trimmed;
|
|
117
|
+
return path.resolve(path.isAbsolute(expanded) ? expanded : path.resolve(repoRoot, expanded));
|
|
118
|
+
}
|
|
119
|
+
export function parseGitWorktreeList(raw) {
|
|
120
|
+
const records = [];
|
|
121
|
+
let current;
|
|
122
|
+
const flush = () => {
|
|
123
|
+
if (current?.path)
|
|
124
|
+
records.push(current);
|
|
125
|
+
current = undefined;
|
|
126
|
+
};
|
|
127
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
128
|
+
if (!line.trim()) {
|
|
129
|
+
flush();
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (line.startsWith("worktree ")) {
|
|
133
|
+
flush();
|
|
134
|
+
current = { path: line.slice("worktree ".length), head: "" };
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!current)
|
|
138
|
+
continue;
|
|
139
|
+
if (line.startsWith("HEAD "))
|
|
140
|
+
current.head = line.slice("HEAD ".length).trim();
|
|
141
|
+
else if (line.startsWith("branch refs/heads/"))
|
|
142
|
+
current.branch = line.slice("branch refs/heads/".length).trim();
|
|
143
|
+
else if (line === "detached")
|
|
144
|
+
delete current.branch;
|
|
145
|
+
else if (line.startsWith("prunable "))
|
|
146
|
+
current.prunable = line.slice("prunable ".length).trim();
|
|
147
|
+
}
|
|
148
|
+
flush();
|
|
149
|
+
return records;
|
|
150
|
+
}
|
|
151
|
+
function listGitWorktrees(repoRoot) {
|
|
152
|
+
const result = runGit(repoRoot, ["worktree", "list", "--porcelain"]);
|
|
153
|
+
if (result.status !== 0)
|
|
154
|
+
throw new Error(gitFailure(result, `git -C ${repoRoot} worktree list --porcelain`));
|
|
155
|
+
return parseGitWorktreeList(result.stdout);
|
|
156
|
+
}
|
|
157
|
+
function readJsonFile(filePath) {
|
|
158
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
159
|
+
}
|
|
160
|
+
function isRecord(value) {
|
|
161
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
162
|
+
}
|
|
163
|
+
function readManifest(manifestPath) {
|
|
164
|
+
try {
|
|
165
|
+
const stat = fs.lstatSync(manifestPath);
|
|
166
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
167
|
+
return { error: `parallel handoff manifest is not a regular file: ${manifestPath}` };
|
|
168
|
+
if (stat.size > MAX_METADATA_FILE_BYTES)
|
|
169
|
+
return { error: `parallel handoff manifest exceeds the ${MAX_METADATA_FILE_BYTES}-byte limit: ${manifestPath}` };
|
|
170
|
+
const parsed = readJsonFile(manifestPath);
|
|
171
|
+
if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.groups)) {
|
|
172
|
+
return { error: `invalid parallel handoff manifest version or groups: ${manifestPath}` };
|
|
173
|
+
}
|
|
174
|
+
return { manifest: parsed };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
177
|
+
if (code === "ENOENT")
|
|
178
|
+
return { error: `parallel handoff manifest not found: ${manifestPath}` };
|
|
179
|
+
return { error: `failed to read parallel handoff manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}` };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function discoverHandoffPaths(repoRoot, input) {
|
|
183
|
+
const paths = [];
|
|
184
|
+
const warnings = [];
|
|
185
|
+
const add = (candidate) => {
|
|
186
|
+
const resolved = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(repoRoot, candidate);
|
|
187
|
+
if (!paths.some((existing) => comparablePath(existing) === comparablePath(resolved)))
|
|
188
|
+
paths.push(resolved);
|
|
189
|
+
};
|
|
190
|
+
if (input.handoffPath)
|
|
191
|
+
add(input.handoffPath);
|
|
192
|
+
for (const candidate of input.handoffPaths ?? [])
|
|
193
|
+
add(candidate);
|
|
194
|
+
const artifactsDir = path.join(repoRoot, PROJECT_SUBAGENTS_RELATIVE_DIR, "artifacts");
|
|
195
|
+
const projectHandoffPath = path.join(artifactsDir, "handoff.json");
|
|
196
|
+
if (fs.existsSync(projectHandoffPath))
|
|
197
|
+
add(projectHandoffPath);
|
|
198
|
+
const handoffsDir = path.join(artifactsDir, "handoffs");
|
|
199
|
+
try {
|
|
200
|
+
const stat = fs.lstatSync(handoffsDir);
|
|
201
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
202
|
+
warnings.push(`ignored non-directory handoff metadata root: ${handoffsDir}`);
|
|
203
|
+
} else {
|
|
204
|
+
const entries = fs.readdirSync(handoffsDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort().slice(0, MAX_DISCOVERED_HANDOFFS);
|
|
205
|
+
for (const entry of entries)
|
|
206
|
+
add(path.join(handoffsDir, entry));
|
|
207
|
+
if (entries.length === MAX_DISCOVERED_HANDOFFS)
|
|
208
|
+
warnings.push(`handoff metadata discovery capped at ${MAX_DISCOVERED_HANDOFFS} files`);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
212
|
+
if (code !== "ENOENT")
|
|
213
|
+
warnings.push(`failed to inspect handoff metadata root ${handoffsDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
214
|
+
}
|
|
215
|
+
return { paths: paths.sort((left, right) => comparablePath(left).localeCompare(comparablePath(right))), warnings: [...new Set(warnings)].sort() };
|
|
216
|
+
}
|
|
217
|
+
function readStatusBesideManifest(manifestPath) {
|
|
218
|
+
const statusPath = path.join(path.dirname(manifestPath), "status.json");
|
|
219
|
+
try {
|
|
220
|
+
const stat = fs.lstatSync(statusPath);
|
|
221
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
222
|
+
return { statusPath, error: `async status is not a regular file: ${statusPath}` };
|
|
223
|
+
if (stat.size > MAX_METADATA_FILE_BYTES)
|
|
224
|
+
return { statusPath, error: `async status exceeds the ${MAX_METADATA_FILE_BYTES}-byte limit: ${statusPath}` };
|
|
225
|
+
const parsed = readJsonFile(statusPath);
|
|
226
|
+
if (!isRecord(parsed) || typeof parsed.state !== "string" || typeof parsed.runId !== "string") {
|
|
227
|
+
return { statusPath, error: `invalid async status beside manifest: ${statusPath}` };
|
|
228
|
+
}
|
|
229
|
+
return { status: parsed, statusPath };
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
232
|
+
if (code === "ENOENT")
|
|
233
|
+
return { statusPath };
|
|
234
|
+
return { statusPath, error: `failed to read async status ${statusPath}: ${error instanceof Error ? error.message : String(error)}` };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function manifestMetadata(manifestPath, manifest, warnings) {
|
|
238
|
+
const records = [];
|
|
239
|
+
const groups = manifest.groups.slice(0, MAX_PLAN_ENTRIES);
|
|
240
|
+
if (manifest.groups.length > groups.length)
|
|
241
|
+
warnings.push(`handoff group discovery capped at ${MAX_PLAN_ENTRIES} groups in ${manifestPath}`);
|
|
242
|
+
for (const group of groups) {
|
|
243
|
+
if (!isRecord(group) || !Array.isArray(group.cleanup?.tasks) || !Array.isArray(group.children)) {
|
|
244
|
+
warnings.push(`ignored malformed handoff group in ${manifestPath}`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const statusInfo = readStatusBesideManifest(manifestPath);
|
|
248
|
+
const tasks = group.cleanup.tasks.slice(0, MAX_PLAN_ENTRIES);
|
|
249
|
+
if (group.cleanup.tasks.length > tasks.length)
|
|
250
|
+
warnings.push(`cleanup task discovery capped at ${MAX_PLAN_ENTRIES} tasks in ${manifestPath}`);
|
|
251
|
+
for (const task of tasks) {
|
|
252
|
+
if (!isRecord(task) || typeof task.path !== "string" || !task.path.trim() || typeof task.branch !== "string" || !task.branch.trim() || typeof task.index !== "number" || !Number.isInteger(task.index) || task.index < 0) {
|
|
253
|
+
warnings.push(`ignored malformed cleanup task in ${manifestPath}`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const matchingChildren = group.children.filter((candidate) => isRecord(candidate) && candidate.taskIndex === task.index);
|
|
257
|
+
if (matchingChildren.length !== 1 || !isRecord(matchingChildren[0]) || typeof matchingChildren[0].status !== "string") {
|
|
258
|
+
warnings.push(`cleanup task ${task.index} does not have exactly one valid child metadata record in ${manifestPath}`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
records.push({
|
|
262
|
+
manifestPath,
|
|
263
|
+
manifest,
|
|
264
|
+
group,
|
|
265
|
+
task,
|
|
266
|
+
child: matchingChildren[0],
|
|
267
|
+
...statusInfo.status ? { status: statusInfo.status } : {},
|
|
268
|
+
statusPath: statusInfo.statusPath,
|
|
269
|
+
...statusInfo.error ? { statusError: statusInfo.error } : {}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return records;
|
|
274
|
+
}
|
|
275
|
+
function loadMetadata(input, repoRoot) {
|
|
276
|
+
const discovered = discoverHandoffPaths(repoRoot, input);
|
|
277
|
+
const records = [];
|
|
278
|
+
const warnings = [...discovered.warnings];
|
|
279
|
+
const explicitPath = input.handoffPath ? path.isAbsolute(input.handoffPath) ? path.resolve(input.handoffPath) : path.resolve(repoRoot, input.handoffPath) : undefined;
|
|
280
|
+
for (const manifestPath of discovered.paths) {
|
|
281
|
+
const result = readManifest(manifestPath);
|
|
282
|
+
if (!result.manifest) {
|
|
283
|
+
if (explicitPath && comparablePath(manifestPath) === comparablePath(explicitPath))
|
|
284
|
+
warnings.push(result.error);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const manifestRoot = resolveExistingPath(manifestPath);
|
|
288
|
+
if (manifestRoot !== manifestPath && fs.lstatSync(manifestPath).isSymbolicLink()) {
|
|
289
|
+
warnings.push(`ignored symlinked handoff manifest: ${manifestPath}`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
records.push(...manifestMetadata(manifestPath, result.manifest, warnings));
|
|
293
|
+
}
|
|
294
|
+
return { records, paths: discovered.paths, warnings: [...new Set(warnings)].sort() };
|
|
295
|
+
}
|
|
296
|
+
function inspectPath(candidate) {
|
|
297
|
+
const resolved = path.resolve(candidate);
|
|
298
|
+
try {
|
|
299
|
+
const stat = fs.lstatSync(resolved);
|
|
300
|
+
if (stat.isSymbolicLink())
|
|
301
|
+
return { resolved, missing: false, symlink: true, directory: false };
|
|
302
|
+
return { resolved, realpath: realpathExisting(resolved), missing: false, symlink: false, directory: stat.isDirectory() };
|
|
303
|
+
} catch (error) {
|
|
304
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
305
|
+
if (code === "ENOENT")
|
|
306
|
+
return { resolved, missing: true, symlink: false, directory: false };
|
|
307
|
+
return { resolved, missing: false, symlink: false, directory: false, error: error instanceof Error ? error.message : String(error) };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function metadataPath(manifestPath, candidate) {
|
|
311
|
+
return path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(path.dirname(manifestPath), candidate);
|
|
312
|
+
}
|
|
313
|
+
function metadataRecordPath(record) {
|
|
314
|
+
return metadataPath(record.manifestPath, record.task.path);
|
|
315
|
+
}
|
|
316
|
+
function metadataPatchPath(record) {
|
|
317
|
+
const patchPath = record.child.patch?.path;
|
|
318
|
+
return typeof patchPath === "string" && patchPath.trim() ? metadataPath(record.manifestPath, patchPath) : undefined;
|
|
319
|
+
}
|
|
320
|
+
function metadataOutputPaths(record) {
|
|
321
|
+
return [record.child.outputPath, record.child.structuredOutputPath, record.child.sessionPath, record.child.patch?.path].filter((candidate) => typeof candidate === "string" && candidate.trim().length > 0).map((candidate) => metadataPath(record.manifestPath, candidate));
|
|
322
|
+
}
|
|
323
|
+
function metadataReportPaths(record) {
|
|
324
|
+
return [record.child.outputPath, record.child.structuredOutputPath, record.child.sessionPath].filter((candidate) => typeof candidate === "string" && candidate.trim().length > 0).map((candidate) => metadataPath(record.manifestPath, candidate));
|
|
325
|
+
}
|
|
326
|
+
function terminalRunState(status) {
|
|
327
|
+
return status === "complete" || status === "failed" || status === "partial" || status === "paused" || status === "stopped" || status === "rejected";
|
|
328
|
+
}
|
|
329
|
+
function inspectRunState(record, now, foregroundRunOwnership) {
|
|
330
|
+
if (record.statusError)
|
|
331
|
+
return { kind: "unknown", reason: record.statusError };
|
|
332
|
+
if (record.status) {
|
|
333
|
+
if (record.status.runId !== record.manifest.runId)
|
|
334
|
+
return { kind: "unknown", reason: `status run id '${record.status.runId}' does not match manifest run '${record.manifest.runId}'` };
|
|
335
|
+
if (record.status.state === "queued" || record.status.state === "running")
|
|
336
|
+
return { kind: "active", reason: `owning run is ${record.status.state}` };
|
|
337
|
+
if (!terminalRunState(record.status.state))
|
|
338
|
+
return { kind: "unknown", reason: `owning run has unknown state '${record.status.state}'` };
|
|
339
|
+
} else if (record.manifest.source === "async") {
|
|
340
|
+
return { kind: "unknown", reason: `async status is missing beside ${record.manifestPath}` };
|
|
341
|
+
}
|
|
342
|
+
if (record.group.children.length === 0 || !record.group.children.every((child) => isTerminalParallelHandoffChildStatus(child.status))) {
|
|
343
|
+
return { kind: "active", reason: "owning handoff still has a non-terminal child" };
|
|
344
|
+
}
|
|
345
|
+
if (record.manifest.source === "foreground") {
|
|
346
|
+
try {
|
|
347
|
+
const ownership = foregroundRunOwnership?.(record.manifest.runId) ?? "unknown";
|
|
348
|
+
if (ownership === "active")
|
|
349
|
+
return { kind: "active", reason: "owning foreground run is still active" };
|
|
350
|
+
if (ownership !== "terminal")
|
|
351
|
+
return { kind: "unknown", reason: "foreground owning-run state is not provably terminal" };
|
|
352
|
+
return { kind: "terminal" };
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return { kind: "unknown", reason: `failed to inspect foreground owning run: ${error instanceof Error ? error.message : String(error)}` };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const markerAge = activeRunMarkerAgeMs(path.dirname(record.statusPath), now);
|
|
359
|
+
if (markerAge !== undefined) {
|
|
360
|
+
if (markerAge <= DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS)
|
|
361
|
+
return { kind: "active", reason: "owning run still has a recent active marker" };
|
|
362
|
+
return { kind: "stale", reason: `terminal active marker is stale (${Math.floor(markerAge / 1000)}s old)` };
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return { kind: "unknown", reason: `failed to inspect active-run marker: ${error instanceof Error ? error.message : String(error)}` };
|
|
366
|
+
}
|
|
367
|
+
return { kind: "terminal" };
|
|
368
|
+
}
|
|
369
|
+
function buildEntryBase(input) {
|
|
370
|
+
const recordedBaseDir = path.dirname(input.path);
|
|
371
|
+
const patchPath = input.metadata ? metadataPatchPath(input.metadata) : undefined;
|
|
372
|
+
return {
|
|
373
|
+
path: input.path,
|
|
374
|
+
branch: input.branch,
|
|
375
|
+
decision: "unknown",
|
|
376
|
+
state: "unknown",
|
|
377
|
+
reasons: [],
|
|
378
|
+
source: input.source,
|
|
379
|
+
...input.metadata ? { runId: input.metadata.manifest.runId, handoffPath: input.metadata.manifestPath, taskIndex: input.metadata.task.index } : {},
|
|
380
|
+
...input.metadata?.group.baseCommit ? { baseCommit: input.metadata.group.baseCommit } : {},
|
|
381
|
+
...patchPath ? { patchPath } : {},
|
|
382
|
+
targetRef: input.targetHead,
|
|
383
|
+
preconditions: {
|
|
384
|
+
path: input.path,
|
|
385
|
+
branch: input.branch,
|
|
386
|
+
...input.git?.head ? { worktreeHead: input.git.head } : {},
|
|
387
|
+
...input.metadata?.group.baseCommit ? { baseCommit: input.metadata.group.baseCommit } : {},
|
|
388
|
+
recordedBaseDir,
|
|
389
|
+
targetRef: input.targetHead
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function blockedEntry(entry, state, decision, reason) {
|
|
394
|
+
return { ...entry, state, decision, reasons: [...entry.reasons, reason] };
|
|
395
|
+
}
|
|
396
|
+
function inspectStatus(worktreePath) {
|
|
397
|
+
const result = runGit(worktreePath, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
398
|
+
if (result.status !== 0)
|
|
399
|
+
return { error: gitFailure(result, `git -C ${worktreePath} status --porcelain=v1`) };
|
|
400
|
+
const output = result.stdout;
|
|
401
|
+
return { output, digest: sha256(output) };
|
|
402
|
+
}
|
|
403
|
+
function resolveCommit(repoRoot, commit) {
|
|
404
|
+
const result = runGit(repoRoot, ["rev-parse", "--verify", `${commit}^{commit}`]);
|
|
405
|
+
if (result.status !== 0)
|
|
406
|
+
return { error: gitFailure(result, `git -C ${repoRoot} rev-parse ${commit}`) };
|
|
407
|
+
return { value: result.stdout.trim() };
|
|
408
|
+
}
|
|
409
|
+
function resolveBranchTip(repoRoot, branch) {
|
|
410
|
+
const result = runGit(repoRoot, ["rev-parse", "--verify", `refs/heads/${branch}^{commit}`]);
|
|
411
|
+
if (result.status !== 0)
|
|
412
|
+
return { error: gitFailure(result, `git -C ${repoRoot} rev-parse refs/heads/${branch}`) };
|
|
413
|
+
return { value: result.stdout.trim() };
|
|
414
|
+
}
|
|
415
|
+
function isPatchCaptured(record, worktreePath) {
|
|
416
|
+
const patchPath = metadataPatchPath(record);
|
|
417
|
+
if (!patchPath || record.child.patch?.error !== undefined || record.child.patch?.changed !== true)
|
|
418
|
+
return {};
|
|
419
|
+
const inspection = inspectPath(patchPath);
|
|
420
|
+
if (inspection.error)
|
|
421
|
+
return { error: `captured patch cannot be inspected: ${inspection.error}` };
|
|
422
|
+
if (inspection.missing || !inspection.realpath)
|
|
423
|
+
return { error: "captured handoff patch is missing" };
|
|
424
|
+
let patchStat;
|
|
425
|
+
try {
|
|
426
|
+
patchStat = fs.statSync(patchPath);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
return { error: `captured patch cannot be inspected: ${error instanceof Error ? error.message : String(error)}` };
|
|
429
|
+
}
|
|
430
|
+
if (!patchStat.isFile())
|
|
431
|
+
return { error: "captured handoff patch is not a file" };
|
|
432
|
+
if (pathInside(worktreePath, inspection.realpath))
|
|
433
|
+
return { error: "durable handoff patch lives inside the worktree" };
|
|
434
|
+
if (patchStat.size <= 0)
|
|
435
|
+
return { error: "captured handoff patch is empty" };
|
|
436
|
+
return { path: patchPath };
|
|
437
|
+
}
|
|
438
|
+
function buildManagedEntry(input) {
|
|
439
|
+
const { repoRoot, baseDir, targetHead, rootPath, git, record, foregroundRunOwnership } = input;
|
|
440
|
+
const gitWorktreePath = path.resolve(git.path);
|
|
441
|
+
const metadataWorktreePath = metadataRecordPath(record);
|
|
442
|
+
const worktreePath = resolveExistingPath(metadataWorktreePath);
|
|
443
|
+
const entry = buildEntryBase({ path: worktreePath, branch: git.branch ?? "", source: "both", git, metadata: record, targetHead });
|
|
444
|
+
if (typeof record.manifest.runId !== "string" || !record.manifest.runId.trim())
|
|
445
|
+
return blockedEntry(entry, "unknown", "unknown", "handoff metadata has no valid owning run id");
|
|
446
|
+
if (record.manifest.source !== "foreground" && record.manifest.source !== "async")
|
|
447
|
+
return blockedEntry(entry, "unknown", "unknown", "handoff metadata has an unknown source");
|
|
448
|
+
if (typeof record.group.repoRoot !== "string" || !record.group.repoRoot.trim())
|
|
449
|
+
return blockedEntry(entry, "unknown", "unknown", "handoff metadata has no repository root");
|
|
450
|
+
if (!samePath(metadataWorktreePath, gitWorktreePath))
|
|
451
|
+
return blockedEntry(entry, "unknown", "unknown", "metadata worktree path does not identify the Git worktree");
|
|
452
|
+
const gitPathInspection = inspectPath(gitWorktreePath);
|
|
453
|
+
if (gitPathInspection.error)
|
|
454
|
+
return blockedEntry(entry, "unknown", "unknown", `Git worktree path could not be inspected: ${gitPathInspection.error}`);
|
|
455
|
+
if (gitPathInspection.missing)
|
|
456
|
+
return blockedEntry(entry, "stale", "unknown", "Git worktree path is missing from disk");
|
|
457
|
+
if (gitPathInspection.symlink)
|
|
458
|
+
return blockedEntry(entry, "unknown", "unknown", "Git worktree path is a symlink; cleanup requires a real directory");
|
|
459
|
+
const pathInspection = inspectPath(worktreePath);
|
|
460
|
+
if (pathInspection.error)
|
|
461
|
+
return blockedEntry(entry, "unknown", "unknown", `worktree path could not be inspected: ${pathInspection.error}`);
|
|
462
|
+
if (pathInspection.missing)
|
|
463
|
+
return blockedEntry(entry, "stale", "unknown", "worktree path is missing from disk");
|
|
464
|
+
if (pathInspection.symlink)
|
|
465
|
+
return blockedEntry(entry, "unknown", "unknown", "worktree path is a symlink; cleanup requires a real directory");
|
|
466
|
+
if (!pathInspection.directory || !pathInspection.realpath)
|
|
467
|
+
return blockedEntry(entry, "unknown", "unknown", "worktree path is not a directory");
|
|
468
|
+
if (!pathInside(baseDir, pathInspection.realpath, true))
|
|
469
|
+
return blockedEntry(entry, "ineligible", "keep", `worktree real path is outside configured base directory '${baseDir}'`);
|
|
470
|
+
if (rootPath === comparablePath(pathInspection.realpath))
|
|
471
|
+
return blockedEntry(entry, "ineligible", "keep", "worktree is the repository root");
|
|
472
|
+
if (!git.branch)
|
|
473
|
+
return blockedEntry(entry, "unknown", "unknown", "detached worktrees have no metadata-recorded branch");
|
|
474
|
+
if (record.task.branch !== git.branch)
|
|
475
|
+
return blockedEntry(entry, "unknown", "unknown", `metadata branch '${record.task.branch}' does not match Git branch '${git.branch}'`);
|
|
476
|
+
if (record.task.worktreeRemoved)
|
|
477
|
+
return blockedEntry(entry, "stale", "unknown", "handoff metadata already records this worktree as removed");
|
|
478
|
+
if (record.task.preserved !== true)
|
|
479
|
+
return blockedEntry(entry, "unknown", "unknown", "handoff metadata does not mark this worktree as preserved");
|
|
480
|
+
if (record.group.cleanup.state !== "partial")
|
|
481
|
+
return blockedEntry(entry, "unknown", "unknown", "handoff cleanup state does not indicate a preserved worktree pending cleanup");
|
|
482
|
+
const cleanupReason = typeof record.task.reason === "string" ? record.task.reason.toLowerCase() : "";
|
|
483
|
+
if (cleanupReason.includes("retained child resume"))
|
|
484
|
+
return blockedEntry(entry, "ineligible", "keep", "retained child resume still requires this worktree cwd");
|
|
485
|
+
if (cleanupReason.includes("cleanup pending durable handoff capture"))
|
|
486
|
+
return blockedEntry(entry, "ineligible", "keep", "durable handoff capture is still pending for this worktree");
|
|
487
|
+
if (record.group.repoRoot && comparablePath(resolveExistingPath(record.group.repoRoot)) !== comparablePath(repoRoot)) {
|
|
488
|
+
return blockedEntry(entry, "unknown", "unknown", "handoff repository root does not match the requested repository");
|
|
489
|
+
}
|
|
490
|
+
if (input.otherGitRecords.some((candidate) => candidate.branch === git.branch && comparablePath(candidate.path) !== comparablePath(git.path))) {
|
|
491
|
+
return blockedEntry(entry, "unknown", "unknown", "metadata-recorded branch is checked out in another Git worktree");
|
|
492
|
+
}
|
|
493
|
+
if (git.branch === input.otherGitRecords.find((candidate) => comparablePath(candidate.path) === rootPath)?.branch) {
|
|
494
|
+
return blockedEntry(entry, "ineligible", "keep", "metadata-recorded branch is checked out at the repository root");
|
|
495
|
+
}
|
|
496
|
+
const runState = inspectRunState(record, input.now, foregroundRunOwnership);
|
|
497
|
+
if (runState.kind === "unknown")
|
|
498
|
+
return blockedEntry(entry, "unknown", "unknown", runState.reason ?? "owning run state is unknown");
|
|
499
|
+
if (runState.kind === "active")
|
|
500
|
+
return blockedEntry(entry, "active", "keep", runState.reason ?? "owning run is active");
|
|
501
|
+
if (runState.kind === "stale")
|
|
502
|
+
return blockedEntry(entry, "stale", "unknown", runState.reason ?? "terminal active marker is stale");
|
|
503
|
+
const baseCommit = record.group.baseCommit;
|
|
504
|
+
if (typeof baseCommit !== "string" || !baseCommit.trim())
|
|
505
|
+
return blockedEntry(entry, "unknown", "unknown", "handoff metadata has no base commit");
|
|
506
|
+
const resolvedBase = resolveCommit(repoRoot, baseCommit);
|
|
507
|
+
if (!resolvedBase.value)
|
|
508
|
+
return blockedEntry(entry, "unknown", "unknown", `handoff base commit is not a valid local commit: ${resolvedBase.error ?? "unknown error"}`);
|
|
509
|
+
entry.baseCommit = resolvedBase.value;
|
|
510
|
+
entry.preconditions.baseCommit = resolvedBase.value;
|
|
511
|
+
const branchTip = resolveBranchTip(repoRoot, git.branch);
|
|
512
|
+
if (!branchTip.value)
|
|
513
|
+
return blockedEntry(entry, "unknown", "unknown", `branch tip could not be resolved: ${branchTip.error ?? "unknown error"}`);
|
|
514
|
+
entry.preconditions.branchTip = branchTip.value;
|
|
515
|
+
entry.preconditions.worktreeHead = git.head;
|
|
516
|
+
if (branchTip.value !== git.head)
|
|
517
|
+
return blockedEntry(entry, "unknown", "unknown", "branch tip and worktree HEAD do not match");
|
|
518
|
+
const status = inspectStatus(worktreePath);
|
|
519
|
+
if (status.error)
|
|
520
|
+
return blockedEntry(entry, "unknown", "unknown", `git status failed: ${status.error}`);
|
|
521
|
+
entry.preconditions.statusDigest = status.digest;
|
|
522
|
+
if (status.output && status.output.trim())
|
|
523
|
+
return blockedEntry(entry, "dirty", "keep", "worktree has uncommitted or untracked changes");
|
|
524
|
+
const diff = runGit(worktreePath, ["diff", "--quiet", resolvedBase.value, "--"]);
|
|
525
|
+
if (diff.status !== 0 && diff.status !== 1)
|
|
526
|
+
return blockedEntry(entry, "unknown", "unknown", `git diff safety check failed: ${gitFailure(diff, "git diff")}`);
|
|
527
|
+
const ancestor = runGit(repoRoot, ["merge-base", "--is-ancestor", git.head, targetHead]);
|
|
528
|
+
if (ancestor.status !== 0 && ancestor.status !== 1)
|
|
529
|
+
return blockedEntry(entry, "unknown", "unknown", `local merge safety check failed: ${gitFailure(ancestor, "git merge-base --is-ancestor")}`);
|
|
530
|
+
const branchTipIsAncestor = ancestor.status === 0;
|
|
531
|
+
let divergenceSafe = diff.status === 0;
|
|
532
|
+
if (!divergenceSafe) {
|
|
533
|
+
const captured = isPatchCaptured(record, worktreePath);
|
|
534
|
+
if (captured.error)
|
|
535
|
+
return blockedEntry(entry, "ineligible", "keep", captured.error);
|
|
536
|
+
if (captured.path) {
|
|
537
|
+
entry.patchPath = captured.path;
|
|
538
|
+
divergenceSafe = true;
|
|
539
|
+
} else {
|
|
540
|
+
if (branchTipIsAncestor)
|
|
541
|
+
divergenceSafe = true;
|
|
542
|
+
else
|
|
543
|
+
return blockedEntry(entry, "ineligible", "keep", "committed divergence is neither preserved by a handoff patch nor merged into the local target HEAD");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (!divergenceSafe)
|
|
547
|
+
return blockedEntry(entry, "unknown", "unknown", "worktree divergence safety could not be established");
|
|
548
|
+
for (const reportPath of [record.manifestPath, ...metadataReportPaths(record)]) {
|
|
549
|
+
const inspection = inspectPath(reportPath);
|
|
550
|
+
if (inspection.error)
|
|
551
|
+
return blockedEntry(entry, "unknown", "unknown", `durable handoff path could not be inspected: ${inspection.error}`);
|
|
552
|
+
const comparable = inspection.realpath ?? (inspection.symlink ? resolveExistingPath(reportPath) : inspection.resolved);
|
|
553
|
+
if (pathInside(pathInspection.realpath, comparable))
|
|
554
|
+
return blockedEntry(entry, "ineligible", "keep", `durable handoff path is inside the worktree: ${reportPath}`);
|
|
555
|
+
if (inspection.missing)
|
|
556
|
+
return blockedEntry(entry, "unknown", "unknown", `durable handoff path is missing: ${reportPath}`);
|
|
557
|
+
if (inspection.symlink)
|
|
558
|
+
return blockedEntry(entry, "unknown", "unknown", `durable handoff path is a symlink: ${reportPath}`);
|
|
559
|
+
}
|
|
560
|
+
for (const durablePath of metadataOutputPaths(record)) {
|
|
561
|
+
const inspection = inspectPath(durablePath);
|
|
562
|
+
const comparable = inspection.realpath ?? (inspection.symlink ? resolveExistingPath(durablePath) : inspection.resolved);
|
|
563
|
+
if (pathInside(pathInspection.realpath, comparable))
|
|
564
|
+
return blockedEntry(entry, "ineligible", "keep", `durable report or patch path is inside the worktree: ${durablePath}`);
|
|
565
|
+
}
|
|
566
|
+
entry.state = "safe";
|
|
567
|
+
entry.decision = "remove";
|
|
568
|
+
entry.willDeleteBranch = branchTipIsAncestor;
|
|
569
|
+
entry.reasons.push("extension-owned metadata, terminal run, contained clean worktree, and local divergence checks passed");
|
|
570
|
+
if (!branchTipIsAncestor)
|
|
571
|
+
entry.reasons.push("local branch tip is not an ancestor of target HEAD; future apply must retain the branch");
|
|
572
|
+
return entry;
|
|
573
|
+
}
|
|
574
|
+
function buildUnknownGitEntry(input) {
|
|
575
|
+
const pathValue = resolveExistingPath(input.git.path);
|
|
576
|
+
const entry = buildEntryBase({ path: pathValue, branch: input.git.branch ?? "", source: "git", git: input.git, targetHead: input.targetHead });
|
|
577
|
+
return blockedEntry(entry, "unknown", "unknown", "no matching extension-owned handoff metadata was found");
|
|
578
|
+
}
|
|
579
|
+
function buildMissingMetadataEntry(input) {
|
|
580
|
+
if (input.record.task.worktreeRemoved && input.record.task.branchRemoved)
|
|
581
|
+
return;
|
|
582
|
+
const pathValue = metadataRecordPath(input.record);
|
|
583
|
+
const entry = buildEntryBase({ path: pathValue, branch: input.record.task.branch, source: "metadata", metadata: input.record, targetHead: input.targetHead });
|
|
584
|
+
return blockedEntry(entry, "stale", "unknown", "handoff metadata records a worktree that is not present in Git worktree state");
|
|
585
|
+
}
|
|
586
|
+
function contentPayload(plan) {
|
|
587
|
+
return {
|
|
588
|
+
version: plan.version,
|
|
589
|
+
repoRoot: plan.repoRoot,
|
|
590
|
+
baseDirs: plan.baseDirs,
|
|
591
|
+
metadataPaths: plan.metadataPaths,
|
|
592
|
+
entries: plan.entries,
|
|
593
|
+
pruneCandidates: plan.pruneCandidates,
|
|
594
|
+
...plan.warnings ? { warnings: plan.warnings } : {}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function stableEntrySort(left, right) {
|
|
598
|
+
return comparablePath(left.path).localeCompare(comparablePath(right.path)) || left.branch.localeCompare(right.branch);
|
|
599
|
+
}
|
|
600
|
+
export function buildWorktreeCleanupPlan(input) {
|
|
601
|
+
if (typeof input.repo !== "string" || !input.repo.trim())
|
|
602
|
+
throw new Error("worktree cleanup plan requires a repository path");
|
|
603
|
+
const repoRoot = resolveRepoRoot(input.repo);
|
|
604
|
+
const now = input.now ?? Date.now();
|
|
605
|
+
if (!Number.isFinite(now))
|
|
606
|
+
throw new Error("worktree cleanup plan timestamp must be finite");
|
|
607
|
+
const baseDir = resolveCleanupBaseDir(repoRoot, input.worktreeBaseDir);
|
|
608
|
+
const gitRecords = listGitWorktrees(repoRoot);
|
|
609
|
+
const targetHead = runGitChecked(repoRoot, ["rev-parse", "HEAD"]);
|
|
610
|
+
const rootPath = comparablePath(repoRoot);
|
|
611
|
+
const metadata = loadMetadata(input, repoRoot);
|
|
612
|
+
const metadataByPath = new Map;
|
|
613
|
+
for (const record of metadata.records) {
|
|
614
|
+
const key = comparablePath(metadataRecordPath(record));
|
|
615
|
+
const records = metadataByPath.get(key) ?? [];
|
|
616
|
+
records.push(record);
|
|
617
|
+
metadataByPath.set(key, records);
|
|
618
|
+
}
|
|
619
|
+
const linkedGit = gitRecords.filter((record) => !samePath(record.path, repoRoot));
|
|
620
|
+
const entries = [];
|
|
621
|
+
const matchedMetadataRecords = new Set;
|
|
622
|
+
for (const git of linkedGit) {
|
|
623
|
+
const records = metadataByPath.get(comparablePath(git.path)) ?? (git.branch ? metadata.records.filter((record) => record.task.branch === git.branch && samePath(metadataRecordPath(record), git.path)) : []);
|
|
624
|
+
if (records.length === 1)
|
|
625
|
+
matchedMetadataRecords.add(records[0]);
|
|
626
|
+
if (records.length !== 1)
|
|
627
|
+
entries.push(buildUnknownGitEntry({ git, targetHead }));
|
|
628
|
+
else
|
|
629
|
+
entries.push(buildManagedEntry({
|
|
630
|
+
repoRoot,
|
|
631
|
+
baseDir,
|
|
632
|
+
targetHead,
|
|
633
|
+
rootPath,
|
|
634
|
+
git,
|
|
635
|
+
record: records[0],
|
|
636
|
+
otherGitRecords: gitRecords,
|
|
637
|
+
now,
|
|
638
|
+
foregroundRunOwnership: input.foregroundRunOwnership
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
const gitPathKeys = new Set(linkedGit.map((record) => comparablePath(record.path)));
|
|
642
|
+
for (const [key, records] of metadataByPath) {
|
|
643
|
+
if (gitPathKeys.has(key) || records.length !== 1 || matchedMetadataRecords.has(records[0]))
|
|
644
|
+
continue;
|
|
645
|
+
const missing = buildMissingMetadataEntry({ record: records[0], targetHead });
|
|
646
|
+
if (missing)
|
|
647
|
+
entries.push(missing);
|
|
648
|
+
}
|
|
649
|
+
entries.sort(stableEntrySort);
|
|
650
|
+
const warnings = metadata.warnings;
|
|
651
|
+
if (entries.length > MAX_PLAN_ENTRIES) {
|
|
652
|
+
warnings.push(`cleanup plan entry count capped at ${MAX_PLAN_ENTRIES}; remaining worktrees are not evaluated`);
|
|
653
|
+
entries.splice(MAX_PLAN_ENTRIES);
|
|
654
|
+
}
|
|
655
|
+
const pruneCandidates = entries.filter((entry) => entry.decision === "unknown" && entry.state === "stale" && gitRecords.some((record) => record.prunable && comparablePath(record.path) === comparablePath(entry.path))).map((entry) => entry.path).sort((left, right) => comparablePath(left).localeCompare(comparablePath(right)));
|
|
656
|
+
const basePlan = {
|
|
657
|
+
version: WORKTREE_CLEANUP_PLAN_VERSION,
|
|
658
|
+
repoRoot,
|
|
659
|
+
baseDirs: [baseDir],
|
|
660
|
+
metadataPaths: metadata.paths,
|
|
661
|
+
entries,
|
|
662
|
+
pruneCandidates,
|
|
663
|
+
...warnings.length ? { warnings: [...new Set(warnings)].sort() } : {}
|
|
664
|
+
};
|
|
665
|
+
const contentHash = sha256(JSON.stringify(contentPayload(basePlan)));
|
|
666
|
+
const planId = validatePlanId(input.planId ?? randomUUID());
|
|
667
|
+
return {
|
|
668
|
+
...basePlan,
|
|
669
|
+
planId,
|
|
670
|
+
createdAt: now,
|
|
671
|
+
expiresAt: now + WORKTREE_CLEANUP_PLAN_TTL_MS,
|
|
672
|
+
contentHash
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
export function worktreeCleanupPlanPath(repoRoot, planId) {
|
|
676
|
+
return path.join(repoRoot, PROJECT_SUBAGENTS_RELATIVE_DIR, "cleanup-plans", `${validatePlanId(planId)}.json`);
|
|
677
|
+
}
|
|
678
|
+
export function createWorktreeCleanupPlan(input) {
|
|
679
|
+
const plan = buildWorktreeCleanupPlan(input);
|
|
680
|
+
const planPath = worktreeCleanupPlanPath(plan.repoRoot, plan.planId);
|
|
681
|
+
writeAtomicJson(planPath, plan);
|
|
682
|
+
return { plan, planPath };
|
|
683
|
+
}
|
|
684
|
+
function formatPlanEntry(entry) {
|
|
685
|
+
const target = entry.branch ? `${entry.path} [${entry.branch}]` : entry.path;
|
|
686
|
+
return `- ${target}: ${entry.reasons.join("; ") || "no reason recorded"}`;
|
|
687
|
+
}
|
|
688
|
+
export function formatWorktreeCleanupPlan(created) {
|
|
689
|
+
const { plan, planPath } = created;
|
|
690
|
+
const removable = plan.entries.filter((entry) => entry.decision === "remove");
|
|
691
|
+
const branches = removable.filter((entry) => entry.willDeleteBranch === true);
|
|
692
|
+
const kept = plan.entries.filter((entry) => entry.decision === "keep");
|
|
693
|
+
const unknown = plan.entries.filter((entry) => entry.decision === "unknown");
|
|
694
|
+
const section = (title, entries, empty) => [
|
|
695
|
+
title,
|
|
696
|
+
...entries.length ? entries.map(formatPlanEntry) : [`- ${empty}`]
|
|
697
|
+
];
|
|
698
|
+
const lines = [
|
|
699
|
+
`Worktree cleanup plan ${plan.planId}`,
|
|
700
|
+
`Repository: ${plan.repoRoot}`,
|
|
701
|
+
`Expires: ${new Date(plan.expiresAt).toISOString()}`,
|
|
702
|
+
`Entries: ${plan.entries.length}`,
|
|
703
|
+
"",
|
|
704
|
+
...section("Will remove", removable, "none"),
|
|
705
|
+
"",
|
|
706
|
+
...section("Will delete local branches", branches, "none (plan-only mode; no branch will be deleted)"),
|
|
707
|
+
"",
|
|
708
|
+
...section("Will keep, with reasons", kept, "none"),
|
|
709
|
+
"",
|
|
710
|
+
...section("Unknown, needs manual review", unknown, "none"),
|
|
711
|
+
"",
|
|
712
|
+
"Prune candidates",
|
|
713
|
+
...plan.pruneCandidates.length ? plan.pruneCandidates.map((candidate) => `- ${candidate}`) : ["- none (plan-only mode does not prune Git metadata)"],
|
|
714
|
+
...plan.warnings?.length ? ["", "Warnings", ...plan.warnings.map((warning) => `- ${warning}`)] : [],
|
|
715
|
+
"",
|
|
716
|
+
`Plan saved: ${planPath}`,
|
|
717
|
+
"Plan-only mode: no worktrees or branches were removed."
|
|
718
|
+
];
|
|
719
|
+
return lines.join(`
|
|
720
|
+
`);
|
|
721
|
+
}
|