多个 Agent 在同一工作目录并行修改代码时,最直接的风险是互相覆盖:一个正在重命名文件,另一个仍按旧路径编辑;两个测试同时改生成物;一个任务的 git status 混入另一个任务的变更。

Git worktree 允许同一仓库同时拥有多个工作目录,每个 linked worktree 通常检出不同分支:

repository
├── main worktree        → main
└── .worktrees/
    ├── auth-refactor    → wt/auth-refactor
    └── api-tests        → wt/api-tests

将任务与 worktree 绑定后,Agent 的文件和命令工具从任务 assignment 中取得工作目录。任务管“做什么”,worktree 管“在哪里做”。

Worktree 共享什么,隔离什么

Linked worktree 不是完整仓库副本。它们共享 Git 对象库和大部分 refs,但拥有各自的:

  • 工作目录;
  • HEAD
  • index;
  • 当前检出分支状态;
  • 未提交文件修改。

因此,两个任务可以分别修改同名文件,而不会直接写进同一个 checkout。

不过它们仍然共享很多资源:

  • 同一 Git repository 的对象和分支引用;
  • 默认共享的 Git config;
  • 父进程可访问的文件系统和凭据;
  • 数据库、网络、端口与外部服务;
  • 包管理器缓存、容器名称和其他全局资源。

Worktree 是 Git 工作目录隔离,不是 OS 沙箱、容器或权限边界。

先识别当前 Git 仓库

一个常见入口是:

git rev-parse --show-toplevel

它返回当前工作树的顶层目录;命令失败通常意味着当前目录不在可用 Git 工作树中,或 Git 环境异常。

调用方不能把所有失败都静默解释为“不是 Git 仓库”。至少应区分:

  • Git 命令不存在;
  • 当前目录不是仓库;
  • 命令超时;
  • 权限错误;
  • 仓库元数据损坏。

在 linked worktree 中,--show-toplevel 返回当前 worktree 顶层,不是主 worktree。若需要共享 Git 元数据目录,应使用 Git 提供的 --git-common-dir 等接口,而不是猜 .git 是目录;linked worktree 中的 .git 通常是指向共享元数据的文件。

创建前必须明确起始状态

典型创建命令是:

git worktree add -b wt/auth-refactor .worktrees/auth-refactor <start-point>

它创建新分支,并在新目录中检出指定 commit 或 ref。

最容易被忽略的一点是:worktree 基于 Git commit/ref 创建,不会自动携带主工作区尚未提交的修改。假设主目录有未提交的新接口,Agent 从 HEAD 创建 worktree 后,那里仍然看不到这项改动。

因此宿主必须显式选择 starting state:

  • 已提交基线:从明确 branch、tag 或 commit 创建;
  • 当前工作树状态:先由用户决定提交、制作 patch、stash 或采用产品支持的 working-tree 快照;
  • 新分支:明确从哪个已提交基线创建;
  • 依赖另一个任务:等待上游分支集成,或选择其 commit 作为起点。

不能静默假设 HEAD 就代表用户当前看到的代码。

分支和路径都需要稳定命名

任务名可能包含空格、斜杠、.. 或其他路径字符,不能直接拼接为目录和分支。运行时应分别验证:

  • worktree 逻辑名称;
  • 目标目录位于允许的根目录内;
  • 分支名通过 git check-ref-format --branch
  • 路径没有与现有 worktree 或普通文件冲突;
  • 名称与已有任务绑定不重复。

当前课程使用受限名称并创建 wt/<name> 分支。Git 默认拒绝把已经在其他 worktree 检出的同一分支再次检出;不要用 --force 绕过这条保护,除非调用者理解多个目录操作同一分支引用的后果。

如果自动生成名称,还要记录人类可读任务 ID 与真实分支、路径之间的映射,不能依赖目录名反推所有权。

任务绑定应晚于 Git 创建成功

安全顺序是:

校验任务 pending、无人认领、尚未绑定

校验名称、分支、路径与起始 ref

执行 git worktree add

用 git worktree list 核验注册信息

最后把 worktree 写入任务记录

若先写任务绑定,再执行 Git 命令,创建失败会让任务指向不存在的目录。

反过来也可能出现部分成功:Git 报错,但分支已经创建,或者 worktree 已注册、目录也已留下。此时不能假装什么都没发生,也不应未经检查自动删除。运行时应报告 partial operation,列出已存在的分支、目录和 Git 注册项,让宿主决定恢复还是清理。

创建接口最好支持幂等重试:相同 task、name、start point 重试时,若现有状态完全匹配就返回已有绑定;状态不一致则停止并报告冲突。

Git 自己的注册信息才是权威来源

旧版实现维护 .worktrees/index.json。额外索引可以记录任务 ID、创建者和策略,但可能与 Git 元数据漂移。

检查真实 worktree 时应优先使用:

git worktree list --porcelain -z

--porcelain 提供面向脚本的稳定格式,-z 使用 NUL 分隔,能安全处理包含换行等特殊字符的路径。

应用索引适合保存 Git 不知道的业务字段;Git worktree list 适合确认路径、HEAD、branch、locked 和 prunable 状态。两者不一致时应进入 reconciliation,而不是任选一边覆盖另一边。

Claim 时建立 cwd lease

任务绑定 worktree 后,Agent 还不能任意操作。当前课程在 claim 成功时解析任务目录,并建立 assignment:

owner → {task_id, cwd}

文件读取、写入、编辑、glob 和 Bash 都从这份 assignment 取得 cwd。没有认领任务的 teammate 不允许使用工作区工具;绑定失效时也不会回退到主仓库。

这份映射可以理解为 cwd lease:

  • 它只对当前 owner 和 task 有效;
  • 换任务后必须失效;
  • 进程恢复时要从任务记录重新核验;
  • 路径必须仍是 Git 注册的目标 worktree;
  • owner 身份应由运行时提供,不能由模型伪造。

仅把 worktree 路径写进 system prompt 不够,真正的工具 handler 必须使用受验证的 cwd。

为什么完成任务后不能立刻释放 cwd

一次模型响应可能包含:

complete_task
read_file 验证产物
git status 检查状态

如果 complete_task handler 立刻删除 assignment,后续同批工具会失去目录,甚至回退到错误位置。

当前课程在任务完成后保留 cwd,直到这一模型轮次结束、队友回到 IDLE 才释放。完成失败时也继续保留,便于 Agent 修正后重试。

这体现了一条通用规则:状态切换要考虑工具批次边界。模型在同一响应中已经生成的后续调用,不会因为前一个 handler 改变状态而自动重写参数。

并行任务仍可能产生逻辑冲突

不同 worktree 避免了直接文件覆盖,却不能避免两个任务同时修改同一接口。最终合并时仍可能出现:

  • 文本冲突;
  • 没有文本冲突但语义不兼容;
  • 数据库迁移顺序冲突;
  • 测试分别通过、合并后失败;
  • 生成物和依赖锁文件冲突。

任务拆分时应明确模块所有权和接口契约。高耦合文件可以只分配给一个任务,其他队友通过消息提出修改需求。

即使每个 worktree 都通过测试,最终仍要在目标集成分支上重新运行完整验证。

Worktree 不隔离运行时资源

两个 Agent 分别在不同目录执行:

pnpm dev

仍可能争用同一个端口。类似问题还包括:

  • 相同数据库和测试数据;
  • 相同 Docker Compose project 名;
  • 相同云环境;
  • 相同临时目录;
  • 相同缓存或输出路径;
  • 相同凭据和速率限制。

完整任务隔离需要为这些资源分配命名空间,例如独立端口、数据库 schema、Compose project、临时目录和测试账号。Worktree 只能解决 Git checkout 这一层。

依赖与构建缓存如何处理

新 worktree 拥有自己的文件目录,因此通常需要自己的 node_modules、虚拟环境或构建输出。包管理器可以共享全局内容寻址 Store,但项目级链接和生成物仍应在各自工作目录建立。

不要直接让多个 worktree 共享一个可变 node_modules 或构建目录,除非工具明确支持并发和路径差异。否则一个任务升级依赖时,另一个任务的运行结果会变得不可重现。

缓存键至少应考虑 lockfile、平台、运行时版本和任务分支状态。

Event log 用于审计,不是事实源

Worktree 生命周期适合记录事件:

{
  "event": "worktree.created",
  "timestamp": "...",
  "taskId": "task_a1b2c3d4",
  "path": ".worktrees/auth-refactor",
  "branch": "wt/auth-refactor",
  "startPoint": "abc123..."
}

还可以记录 create requested、partial failure、claimed、completed、released、integration verified 和 removed。

JSONL 适合追加和人工检查,但并不会天然可靠:并发写入需要锁,崩溃可能留下不完整行,读取时跳过坏行会损失审计信息。高要求系统应使用具备事务与访问控制的事件存储。

事件回答“发生过什么”,Git 注册信息回答“现在有哪些 worktree”,任务 Store 回答“业务上绑定给谁”。三者职责不同。

任务完成不等于成果已经集成

一个任务可以在自己的分支上完成,但主分支仍然看不到这些修改。收尾至少分成:

task work completed

worktree 内验证通过

审查 diff 与提交

合并、rebase 或 cherry-pick 到集成分支

在集成状态重新验证

任务验收完成

是否允许 Agent 自动提交、合并或解决冲突,应由项目工作流和用户授权决定。完成任务不应暗中执行 Git push 或删除分支。

如果没有提交,移除 worktree 可能直接丢失成果;如果已经提交但未合并,分支必须保留并明确记录。

安全移除的默认策略

Git 官方行为是:git worktree remove 默认只移除干净的 linked worktree;有 tracked 修改或 untracked 文件时会拒绝,--force 可以覆盖这一保护。主 worktree不能被该命令移除。

自动化宿主应在调用前做更严格的预检:

  • 任务不能仍为 pending 或 in-progress;
  • 没有活跃 cwd lease;
  • git status --porcelain 没有 tracked、untracked 或需要保留的 ignored 文件;
  • 重要提交已经集成或明确保留分支;
  • 用户知道将删除哪个目录;
  • 路径仍位于受控 worktree 根目录;
  • Git 注册信息与业务索引一致。

不要把 --force 当作常规清理。带丢弃改动的移除属于破坏性操作,必须由用户明确批准,并在操作后报告哪些内容不可恢复。

移除 worktree 不会删除分支

git worktree remove .worktrees/auth-refactor

移除的是 linked worktree 目录和对应管理记录,不会自动执行:

git branch -d wt/auth-refactor

保留分支通常是更安全的默认值,尤其当分支含有尚未合并或没有 upstream 的提交。

分支删除应是独立决策:先确认成果已集成且分支不再需要,再由用户或受控宿主执行。强制删除分支与强制移除目录不应合并成一个模糊的“cleanup=true”。

Lock、prune 与 repair 解决什么

Git 还提供几个生命周期工具:

  • git worktree lock --reason ...:防止可移动磁盘或暂时离线路径的管理记录被 prune,也阻止普通 move/remove;
  • git worktree prune --dry-run:预览会清理哪些缺失 worktree 的陈旧管理记录;
  • git worktree repair:主仓库或 worktree 被手工移动后,尝试修复双方连接;
  • git worktree list --porcelain:检查 locked、prunable、branch 和 HEAD。

不要通过文件系统直接删除 .worktrees/<name> 来代替 git worktree remove。手工删除会留下 Git 管理记录,后续只能 prune 或 repair。

Locked worktree 的强制移除需要更强覆盖,正说明 lock 是防误操作机制,不是安全权限系统。

Submodule 和配置边界

Git 官方文档说明 multiple checkout 对 submodule 的支持仍不完整,不建议把包含 submodule 的 superproject 随意做多份 checkout。采用前应在目标仓库和 Git 版本上实际测试初始化、更新、移动和清理流程。

Git config 默认在 worktree 之间共享。需要不同配置时,可以使用 Git 的 worktree-specific config 机制,但启用方式和兼容性要明确。不要以为在一个 worktree 执行普通 git config 一定只影响该目录。

建议覆盖的测试

Worktree Task Isolation 至少应验证:

  • 非 Git 目录、Git 不可用、超时和损坏元数据;
  • 主 worktree 与 linked worktree 的根目录识别;
  • 明确 branch、commit 和 working-tree snapshot 起点;
  • 主目录未提交修改不会静默出现在新 worktree;
  • 名称、分支和目标路径校验;
  • 同一分支已在其他 worktree 检出时拒绝;
  • Git 创建成功后才写任务绑定;
  • 分支已创建、worktree 未完成等 partial failure;
  • 重试的幂等性与状态冲突;
  • Git worktree list 与应用索引 reconciliation;
  • claim 建立正确 cwd lease,无任务时工具被拒绝;
  • worktree 失效时不回退主仓库;
  • complete 后在工具批次结束前保留 assignment;
  • 多个 worktree 修改相同接口的合并与语义冲突;
  • 端口、数据库、容器、缓存和凭据冲突;
  • worktree 内验证与集成分支验证分开;
  • dirty、untracked、ignored、locked 和含 submodule 的移除;
  • remove 后分支仍然存在;
  • prune dry-run、repair 和异常移动;
  • 强制丢弃必须有精确用户批准和审计记录。

小结

Git worktree 为 Agent 任务提供的是独立 checkout:每个任务可以绑定自己的分支和目录,claim 后由 cwd lease 确保工具落在正确位置。它能显著减少并行文件覆盖,却不会继承主工作区未提交改动,也不隔离端口、数据库、凭据和系统权限。

可靠生命周期需要明确起始 commit、创建成功后再绑定、识别部分失败、在任务目录与集成状态分别验证,并把任务完成、成果集成、worktree 移除和分支删除拆成独立动作。清理默认保守,任何强制丢弃都应回到用户手里。

参考资料