You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
claude-code/docs/en/s12-worktree-task-isolation.md

6.2 KiB

s12: Worktree + Task Isolation

s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]

"Each works in its own directory, no interference" -- tasks manage goals, worktrees manage directories, bound by ID.

Harness layer: Directory isolation -- parallel execution lanes that never collide.

Problem

By s11, agents can claim and complete tasks autonomously. But every task runs in one shared directory. Two agents refactoring different modules at the same time will collide -- agent A edits Config.java, agent B also edits Config.java, unstaged changes mix, and neither can roll back cleanly.

The task board tracks what to do but has no opinion about where to do it. The fix: give each task its own git worktree directory. Tasks manage goals, worktrees manage execution context. Bind them by task ID.

Solution

Control plane (.tasks/)             Execution plane (.worktrees/)
+------------------+                +------------------------+
| task_1.json      |                | auth-refactor/         |
|   status: in_progress  <------>   branch: wt/auth-refactor
|   worktree: "auth-refactor"   |   task_id: 1             |
+------------------+                +------------------------+
| task_2.json      |                | ui-login/              |
|   status: pending    <------>     branch: wt/ui-login
|   worktree: "ui-login"       |   task_id: 2             |
+------------------+                +------------------------+
                                    |
                          index.json (worktree registry)
                          events.jsonl (lifecycle log)

State machines:
  Task:     pending -> in_progress -> completed
  Worktree: absent  -> active      -> removed | kept

How It Works

  1. Create a task. Persist the goal first.
// src/main/java/io/mybatis/learn/s12/WorktreeTaskManager.java
tasks.create("Implement auth refactor", "");
// -> .tasks/task_1.json  status=pending  worktree=""
  1. Create a worktree and bind to the task. Passing task_id auto-advances the task to in_progress.
// src/main/java/io/mybatis/learn/s12/WorktreeManager.java
worktrees.create("auth-refactor", 1, "HEAD");
// -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD
// -> index.json gets new entry, task_1.json gets worktree="auth-refactor"

The binding writes state to both sides:

// src/main/java/io/mybatis/learn/s12/WorktreeTaskManager.java
public String bindWorktree(int taskId, String worktree, String owner) {
    var task = load(taskId);
    task.put("worktree", worktree);
    if (owner != null && !owner.isEmpty()) task.put("owner", owner);
    if ("pending".equals(task.get("status"))) task.put("status", "in_progress");
    task.put("updated_at", System.currentTimeMillis() / 1000.0);
    save(task);
    return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(task);
}
  1. Run commands in the worktree. cwd points to the isolated directory.
// src/main/java/io/mybatis/learn/s12/WorktreeManager.java - run()
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
ProcessBuilder pb = isWindows
        ? new ProcessBuilder("cmd", "/c", command)
        : new ProcessBuilder("sh", "-c", command);
pb.directory(path.toFile());
pb.redirectErrorStream(true);
Process p = pb.start();
String out = new String(p.getInputStream().readAllBytes()).trim();
boolean finished = p.waitFor(300, java.util.concurrent.TimeUnit.SECONDS);
  1. Close out. Two choices:
    • worktree_keep(name) -- preserve the directory for later.
    • worktree_remove(name, complete_task=True) -- remove directory, complete the bound task, emit event. One call handles teardown + completion.
// src/main/java/io/mybatis/learn/s12/WorktreeManager.java
public String remove(String name, boolean force, boolean completeTask) {
    var wt = findWorktree(name);
    events.emit("worktree.remove.before", ...);
    runGit("worktree", "remove", wt.get("path").toString());
    if (completeTask && wt.get("task_id") != null) {
        int taskId = ((Number) wt.get("task_id")).intValue();
        tasks.update(taskId, "completed", null);
        tasks.unbindWorktree(taskId);
        events.emit("task.completed",
                Map.of("id", taskId, "status", "completed"),
                Map.of("name", name), null);
    }
    // Update index.json: status -> "removed"
}
  1. Event stream. Every lifecycle step emits to .worktrees/events.jsonl:
{
  "event": "worktree.remove.after",
  "task": {"id": 1, "status": "completed"},
  "worktree": {"name": "auth-refactor", "status": "removed"},
  "ts": 1730000000
}

Events emitted: worktree.create.before/after/failed, worktree.remove.before/after/failed, worktree.keep, task.completed.

After a crash, state reconstructs from .tasks/ + .worktrees/index.json on disk. Conversation memory is volatile; file state is durable.

What Changed From s11

Component Before (s11) After (s12)
Coordination Task board (owner/status) Task board + explicit worktree binding
Execution scope Shared directory Task-scoped isolated directory
Recoverability Task status only Task status + worktree index
Teardown Task completion Task completion + explicit keep/remove
Lifecycle visibility Implicit in logs Explicit events in .worktrees/events.jsonl

Try It

cd learn-claude-code
mvn exec:java -Dexec.mainClass=io.mybatis.learn.s12.S12WorktreeIsolation

Try these prompts (English prompts work better with LLMs, but Chinese also works):

  1. Create tasks for backend auth and frontend login page, then list tasks.
  2. Create worktree "auth-refactor" for task 1, then bind task 2 to a new worktree "ui-login".
  3. Run "git status --short" in worktree "auth-refactor".
  4. Keep worktree "ui-login", then list worktrees and inspect events.
  5. Remove worktree "auth-refactor" with complete_task=true, then list tasks/worktrees/events.