From 5853539cab0e990032eb28b29c911ad1650a8fbd Mon Sep 17 00:00:00 2001 From: cexll Date: Thu, 5 Feb 2026 23:32:52 +0800 Subject: [PATCH] fix(do): reuse worktree across phases via DO_WORKTREE_DIR env var Previously, each codeagent-wrapper --worktree call created a new worktree, causing multiple worktrees per /do task (one per phase). Changes: - setup-do.py: create worktree at initialization, export DO_WORKTREE_DIR - executor.go: check DO_WORKTREE_DIR first, reuse if set - SKILL.md: update documentation for new behavior Generated with SWE-Agent.ai Co-Authored-By: SWE-Agent.ai --- .../internal/executor/executor.go | 9 ++++- skills/do/SKILL.md | 11 ++++-- skills/do/scripts/setup-do.py | 38 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/codeagent-wrapper/internal/executor/executor.go b/codeagent-wrapper/internal/executor/executor.go index bff6adc..c189297 100644 --- a/codeagent-wrapper/internal/executor/executor.go +++ b/codeagent-wrapper/internal/executor/executor.go @@ -941,8 +941,13 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe cfg.WorkDir = defaultWorkdir } - // Handle worktree mode: create a new git worktree and update cfg.WorkDir - if taskSpec.Worktree { + // Handle worktree mode: check DO_WORKTREE_DIR env var first, then create if needed + if worktreeDir := os.Getenv("DO_WORKTREE_DIR"); worktreeDir != "" { + // Use existing worktree from /do setup + cfg.WorkDir = worktreeDir + logInfo(fmt.Sprintf("Using existing worktree from DO_WORKTREE_DIR: %s", worktreeDir)) + } else if taskSpec.Worktree { + // Create new worktree (backward compatibility for standalone --worktree usage) paths, err := createWorktreeFn(cfg.WorkDir) if err != nil { result.ExitCode = 1 diff --git a/skills/do/SKILL.md b/skills/do/SKILL.md index 9d97a0c..c7d6d4c 100644 --- a/skills/do/SKILL.md +++ b/skills/do/SKILL.md @@ -41,10 +41,15 @@ This creates `.claude/do.{task_id}.local.md` with: ## Worktree Mode -When `use_worktree: true` in state file, ALL `codeagent-wrapper` calls that modify code MUST include `--worktree`: +When `use_worktree: true` in state file: +- The worktree is created once during initialization (setup-do.py) +- The worktree path is stored in `worktree_dir` frontmatter field +- Environment variable `DO_WORKTREE_DIR` is exported for codeagent-wrapper to use +- ALL `codeagent-wrapper` calls that modify code MUST include `--worktree` flag +- codeagent-wrapper detects `DO_WORKTREE_DIR` and reuses the existing worktree instead of creating new ones ```bash -# With worktree mode enabled +# With worktree mode enabled - codeagent-wrapper will use DO_WORKTREE_DIR automatically codeagent-wrapper --worktree --agent develop - . <<'EOF' ... EOF @@ -60,7 +65,7 @@ workdir: . EOF ``` -The `--worktree` flag tells codeagent-wrapper to create/use a worktree internally. Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `--worktree`. +The `--worktree` flag tells codeagent-wrapper to use worktree mode. When `DO_WORKTREE_DIR` is set, it reuses that directory; otherwise it creates a new worktree (backward compatibility). Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `--worktree`. ## Loop State Management diff --git a/skills/do/scripts/setup-do.py b/skills/do/scripts/setup-do.py index 1c48c6f..2fe2e2d 100755 --- a/skills/do/scripts/setup-do.py +++ b/skills/do/scripts/setup-do.py @@ -2,6 +2,7 @@ import argparse import os import secrets +import subprocess import sys import time @@ -20,6 +21,34 @@ def die(msg: str): print(f"❌ {msg}", file=sys.stderr) sys.exit(1) + +def create_worktree(project_dir: str, task_id: str) -> str: + """Create a git worktree for the task. Returns the worktree directory path.""" + # Get git root + result = subprocess.run( + ["git", "-C", project_dir, "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + die(f"Not a git repository: {project_dir}") + git_root = result.stdout.strip() + + # Calculate paths + worktree_dir = os.path.join(git_root, ".worktrees", f"do-{task_id}") + branch_name = f"do/{task_id}" + + # Create worktree with new branch + result = subprocess.run( + ["git", "-C", git_root, "worktree", "add", "-b", branch_name, worktree_dir], + capture_output=True, + text=True, + ) + if result.returncode != 0: + die(f"Failed to create worktree: {result.stderr}") + + return worktree_dir + def main(): parser = argparse.ArgumentParser( description="Creates (or overwrites) project state file: .claude/do.local.md" @@ -50,6 +79,11 @@ def main(): os.makedirs(state_dir, exist_ok=True) + # Create worktree if requested (before writing state file) + worktree_dir = "" + if use_worktree: + worktree_dir = create_worktree(project_dir, task_id) + phase_name = phase_name_for(1) content = f"""--- @@ -59,6 +93,7 @@ phase_name: "{phase_name}" max_phases: {max_phases} completion_promise: "{completion_promise}" use_worktree: {str(use_worktree).lower()} +worktree_dir: "{worktree_dir}" --- # do loop state @@ -80,6 +115,9 @@ use_worktree: {str(use_worktree).lower()} print(f"completion_promise: {completion_promise}") print(f"use_worktree: {use_worktree}") print(f"export DO_TASK_ID={task_id}") + if worktree_dir: + print(f"worktree_dir: {worktree_dir}") + print(f"export DO_WORKTREE_DIR={worktree_dir}") if __name__ == "__main__": main()