前阵子我把一组开发任务交给 AI Agent 并行处理,结果差点把仓库折腾到还原。三个代理各自在同一个工作目录里改代码、装依赖、跑测试,不到半小时git status就呈现出一副群魔乱舞的状态,package.json被反复覆盖,src/index.ts里两段不相关的逻辑挤在一起,最后连谁改了什么、哪个任务完成了都分不清。后来我把思路换成了 Git Worktree 这套机制,又动手把常用的操作封装成一个管理 CLI,也就是 Worktrunk,整个并行流程才算理顺。这篇东西就把这个工具的来龙去脉、核心设计、实操步骤和踩坑经验完整写出来,给正在用 Claude Code、Codex CLI 这类 AI 编程工具做并行开发的团队做个参考。
1. 并行 AI Agent 编程的冲突:问题是怎么发生的
1.1 多个 Agent 同时工作时最常见的三类冲突
先别急着上方案,得把问题本身拆清楚。我观察到的冲突基本逃不出下面三类。
第一类是同文件并发编辑。两个代理同时接到“修复登录接口超时”和“重构错误处理中间件”的任务,听起来不相干,但它们都绕不开lib/request.ts这个公共文件。AI Agent 不像人在编辑时会先看对方有没有在改,它只认当前工作区里的文件快照,于是各自基于同一份旧内容生成新代码,后写的人直接把先写的人的成果覆盖掉。Git 的合并机制对这种覆盖无能为力,因为文件在工作区层面就已经被互相踩坏了。
第二类是全局状态互相污染。这类问题更隐蔽。一个代理在加新依赖时修改了package.json和pnpm-lock.yaml,另一个代理启动测试脚本时发现依赖树不一致,误以为是自己的改动导致的,于是花大量时间去排查一个根本不是自己制造的问题,甚至可能把依赖“修正”回去,直接破坏前一个代理的开发环境。
第三类是提交历史混杂。所有代理如果都在同一个分支上提交,commit 记录会交织在一起,code review 根本没法按任务维度去筛选差异。三个任务混在一条分支上,其中一个要回退,就得手工挑 commit,稍有疏漏就把其他任务的内容也一起回退了。
把这三类冲突放一起看,本质上都是“所有代理共享同一份物理工作区”造成的。要治本,就必须让每个代理拥有独立的物理工作区,而不是只开一个逻辑上的分支。
1.2 为什么“开多个分支”解决不了并行问题
很多人(包括我开始时)的第一个反应是:给每个任务开个分支不就行了?分支确实在 Git 逻辑上做了隔离,但注意,分支隔离的是提交历史,不是工作区文件。
你在一个已有代码的目录里新建分支git checkout -b task/login-fix,工作区里的文件也会随之切换。如果两个代理在同一个目录里先后切换分支,或者干脆各自在不同的分支上工作但共享目录,物理层面还是同一套文件。代理 A 创建的文件,代理 B 能看见;代理 A 生成的编译产物,代理 B 的测试脚本会用上。隔离了个寂寞。
我打个比方:分支像是一套房子里给不同人分的记事本,大家各写各的,但住在同一间屋子里,桌面只有一个,你放上去的杯子我随手就拿走了。只有再分配一个独立的房间,物理隔开,才算真正互不干扰。
1.3 AI 编程工具的无头运行模式与隔离需求
当前主流的 AI 编程 CLI 工具,比如 Claude Code 和 Codex CLI,都支持在指定目录下以命令行方式启动,并执行一次性任务或进入交互式会话。这意味着你完全可以在不同目录各启动一个代理实例,让它们各自独立工作。
但这里有个前提:你得先把这些目录准备好,并且管理好它们各自的分支归属、状态追踪和收尾动作。手动去git worktree add、手动切换目录、手动检查哪些代理完成了、手动合并回主分支……任务一多,这些机械操作的成本就盖过了并行带来的收益。而 Agent 本身也缺乏一套统一的调用接口去感知“现在哪些任务在跑、哪些工作区改了什么、该往哪个分支合”。
这就是 Worktrunk 想解决的问题:把“为每个 Agent 任务分配独立 Git Worktree、追踪其状态、完成后续合并清理”这套流程封装成一个小型 CLI,让 AI 工具随时可以通过命令感知和操作整个并行工作流。
到这里也该点一下新手常混淆的概念:常说“用 Agent 开发”,其实 Agent 本身不是一个模型,它是由 LLM 驱动并组合了工具调用、技能和上下文的执行系统。DeepSeek、GPT 这类是模型,Claude Code、Codex 这些才是我们讨论的 Agent 载体。所以下面要管理的“代理”,指的是跑这些 CLI 工具的实例,模型是谁在底层驱动并不影响工作区隔离的方案。
2. Git Worktree 才是隔离的底层解法
2.1 Worktree 机制:一次检出,多份工作区
Git Worktree 是 Git 自带的机制,允许你在同一个仓库下维护多个工作目录,这些目录共享同一个.git对象库,但各自拥有独立的文件快照、暂存区和当前分支。
创建的命令很直观:
git worktree add ../repo-hotfix-fix-login fix-login-bug执行后,../repo-hotfix-fix-login目录会被创建,并检出fix-login-bug分支的内容。这里有几个关键机制值得展开:
- 新增的 worktree 会在
.git/worktrees/目录下记录一份元数据,包括它对应的主仓库路径、HEAD 位置、检出的分支等。 - 同一时刻,同一个分支只能被一个 worktree 检出。这是 Git 强制保证的,防止两处工作区对同一分支产生分歧。
- 主工作区(也就是最初 clone 出来的那个目录)不需要显式登记,它本身就是第一个 worktree。
用git worktree list可以看全仓库的所有工作区,git worktree list --porcelain则输出适合脚本解析的机器可读格式。Worktrunk 的命令层大量依赖后者来做状态查询。
2.2 Worktree 与其他隔离方案的对比
好,直接上一份对比表,把我实测过的几种常见方案列出来:
| 隔离方案 | 物理工作区隔离 | 对象库共享 | 依赖安装开销 | 适配 AI Agent 的友好度 |
|---|---|---|---|---|
| 多分支同目录 | 无 | 是 | 无额外开销 | 低,互相踩踏 |
| 多个 Clone | 有 | 否 | 每个目录完整装一遍 | 中,磁盘和网络开销大 |
| Docker / 容器 | 有 | 否 | 镜像层可复用但构建复杂度高 | 中高,环境隔离彻底但重 |
| Git Worktree | 有 | 是 | 每个目录独立安装,但可配置共享缓存 | 高,轻量且是 Git 原生能力 |
多个 clone 的问题在于对象库完全独立,git fetch之后每个目录都要更新一遍,远程地址、分支追踪关系都要单独配,磁盘占用也成倍增加。容器隔离确实彻底,但你得维护镜像、挂载目录、做端口映射,对日常开发任务的并行来说有点杀鸡用牛刀。
Git Worktree 正好卡在中间:目录级物理隔离足够解决文件互相踩踏的问题,共享对象库让 fetch 一次、处处可见,整个过程零网络开销、零额外进程,对 Agent 工具来说不过就是换了启动目录。
2.3 原生命令的不足:为什么需要封装层
既然 Git Worktree 这么能打,直接用原生命令不就行了?理论上可以,但实操层面很难受。
第一个痛点是命名和管理没有语义。git worktree add ../wt-fix-login创建完,留下的目录名和分支名要靠你人脑记住对应哪个任务。跑三个、五个代理的时候,你光想清楚哪个目录对应哪个任务就够折腾了。
第二个痛点是状态无法一眼看清。原生命令不告诉你某个 worktree 里还有没有未提交的改动、本地分支领先还是落后远端、最后提交是什么时候。你只能挨个cd进去查。批量巡检多个 agent 的工作状态,这个动作的效率几乎为零。
第三个痛点是缺少针对 AI Agent 流程的约定。agent 跑完任务可能退出码非零,可能产生了大量 dirty 文件,也可能需要先把主分支的最新提交 rebase 进来。这些状态转换逻辑如果用脚本反复粘贴复制,每换一个项目就来一遍,很容易出错。
所以 Worktrunk 不是要替代 Git Worktree,而是在它之上补一层更懂“并行任务”的壳——这也是整个工具的核心定位。
3. Worktrunk 的设计:从裸命令到 Agent 工作流编排
3.1 领域模型:Task、Worktree、Agent、Base Branch 四要素
设计一个 CLI 最关键的是先定义清楚抽象对象。Worktrunk 的领域模型围绕四个概念展开:
- Task(任务):一个要由代理完成的原子任务,通常对应一段自然语言描述或一个 issue。Task 是 Worktrunk 的最小管理单元。
- Worktree:为 Task 分配出的独立物理工作目录。一个 Task 对应一个 Worktree,两者严格一一绑定。
- Agent:执行这个任务的代理载体标识,可以是
claude、codex、gemini-cli,也可以是本地自定义脚本。Worktrunk 记录这个标识,但不绑定具体实现,方便未来接入新工具。 - Base Branch(基分支):Task 从哪个分支拉出来,任务完成后又合回哪里,通常默认是
main。
这四个足够描述绝大多数场景。不需要引入复杂的图模型,一个 Task 从创建到回收,状态机大致是这样的:
provisioned(已分配工作区) -> active(代理正在执行) -> ready(改动已完成) -> merged(已合并回基分支) / abandoned(已废弃并回收)在provisioned到ready之间还允许sync操作,也就是把基分支的最新提交合并或 rebase 进当前任务分支,避免代理基于过旧的主线代码开发。
3.2 核心命令族的拆解
基于上面的模型,Worktrunk 的命令设计围绕任务生命周期展开,核心命令可以分成三类。
生命周期类,负责创建和销毁任务工作区:
# 创建新任务工作区,自动创建分支并检出 worktrunk task new fix-login-bug --base main --agent claude # 列出全部任务及其状态 worktrunk task list --json # 删除已合并或废弃的任务工作区 worktrunk task cleanup fix-login-bug同步与提交类,负责代理工作完成后的代码归并准备:
# 将基分支最新提交同步进当前任务分支,默认使用 rebase 方式 worktrunk task sync fix-login-bug --method rebase # 查看某个任务的改动统计,确认代理动了哪些文件 worktrunk task diff --stat fix-login-bug # 将任务分支合并回基分支 worktrunk task merge fix-login-bug --method merge巡检类,负责批量查看各代理的运行进展:
# 查看所有任务的状态汇总 worktrunk task list # 查看某个任务工作区内的最近提交记录 worktrunk task log fix-login-bug命令设计的原则就一条:高频动作参数最少。task new只要求任务名,基分支默认取仓库默认分支,代理默认从上一次使用的记录里读取,只有特殊场景才需要显式传参。这能显著降低 Agent 调用时的认知负担。
3.3 状态追踪与回收策略
状态追踪是 Worktrunk 区别于“一堆 git 命令的别名脚本”的关键。工具在.git/worktrunk/state.json中持久化每个 Task 的元信息,包括创建时间、关联目录、基分支、代理标识和当前状态机位置。
为什么状态文件要放.git目录而不是项目根目录?原因是.git目录天然不会被提交、不会被代理改动、也不容易被用户误删,把状态和 Git 元数据放在一起,能保证“只要仓库还在,管理状态就在”。
回收策略上,Worktrunk 采用两种触发方式:
- 显式回收:
worktrunk task cleanup删除已完成合并或废弃任务的工作区,并清理对应分支。 - 隐式校验:每次执行
task list或task new时,自动检测git worktree prune能够处理的孤儿记录,避免状态文件与实际仓库脱节。
对于 task 合并失败的情况,Worktrunk 会把状态置为conflict,保留工作区,等开发者手动解决冲突后再执行worktrunk task merge --continue。这个设计避免了代理任务合并冲突时匆忙强行合并数据丢失的问题。
3.4 面向 Agent 的能力暴露:Skill 与 MCP
CLI 本身做得再顺手,最终使用的可能不是人,而是另一个 Agent。所以 Worktrunk 还需要解决“Agent 怎么学会用这个工具”的问题。
目前主流的 AI 编程 CLI 都支持技能目录(Skill)或者 MCP 工具注册。Worktrunk 提供一套预置的 Skill 文件,内容大致是把 Task 生命周期封装成自然语言形式的操作指南,放进 Agent 的技能配置目录后,代理就能理解“新建独立工作区、在独立目录中执行任务、完成后同步并合并”这一整套约束。
同时,Worktrunk 也暴露一个轻量的 MCP Server 端点,把create_worktree、get_worktree_status、sync_task、merge_task这几个核心动作注册为 MCP 工具。这样无论是支持 MCP 的 Claude Desktop,还是其他 Agent 框架,都可以通过标准协议调用 Worktrunk,而不是去拼 shell 命令。
这一步很重要,因为并行开发的收益图景本来应该是“人定义任务,Agent 各自执行,机器管理过程”。如果过程管理还要人手动操作,效率就打了个大折扣。
4. 实操:用 Worktrunk 跑通一个三 Agent 并行开发流程
4.1 环境准备与初始化配置
先说明一下工具本身的安装。按当前主流 Rust/Go CLI 项目的通用发布方式,Worktrunk 可以通过 Homebrew 或 Cargo 安装:
brew install worktrunk # 或者 cargo install worktrunk安装后在项目根目录执行初始化:
cd /path/to/your-project worktrunk init初始化会做两件事:检测当前仓库是否干净,确认它是一个有效的 Git 仓库;生成一份.worktrunk.yaml配置文件。一个典型的配置长这样:
default-base: main agents: - name: claude command: claude - name: codex command: codex workspace-root: .worktrees dependency-install: run: pnpm install这里workspace-root指定所有 worktree 统一放在项目根目录的.worktrees文件夹内,路径集中,删除清理也方便。dependency-install里的命令会在每次创建新 worktree 后自动执行,让每个代理的工作目录一开始就有完整依赖。
4.2 创建三个任务工作区
现在模拟一个真实场景:有三个任务要并行开展,分别是修复登录接口超时、新增数据导出 API、补充单元测试覆盖率。
需要用 Worktrunk 创建三个任务:
worktrunk task new fix-login-timeout --base main --agent claude worktrunk task new add-export-api --base main --agent codex worktrunk task new improve-test-coverage --base main --agent claude每执行一条命令,Worktrunk 内部会做这些事:
- 读取配置,确认基分支是
main; - 执行
git fetch获取远端最新提交; - 创建新分支并添加到
.worktrees目录下; - 在 worktree 内执行
pnpm install(依据配置); - 把 Task 元信息写入状态文件,状态置为
provisioned。
完成后worktrunk task list可以看到三个任务都处于待执行状态,目录结构大概是:
your-project/ ├── .git/ ├── .worktrees/ │ ├── fix-login-timeout/ │ ├── add-export-api/ │ └── improve-test-coverage/ ├── .worktrunk.yaml └── src/4.3 并行执行与状态巡检
任务创建工作区到位后,就可以分别启动代理了。这里要注意,每个 Agent 必须在对应的 worktree 目录内启动:
# 终端 A cd .worktrees/fix-login-timeout claude -p "修复登录接口在并发请求下超时的问题,改完后运行测试并提交" # 终端 B cd .worktrees/add-export-api codex exec "新增 /api/export 数据导出接口,支持 CSV 格式,记得补充 swagger 注释" # 终端 C cd .worktrees/improve-test-coverage claude -p "为 utils/ 目录下所有函数补充单元测试,覆盖率目标 90% 以上"三个代理各自在隔离的物理目录中运行,互不可见,也就不会互相影响。
巡检时在项目根目录执行:
worktrunk task list输出类似:
Task Directory Status Ahead/Behind Last Commit Agent fix-login-timeout .worktrees/fix-login-timeout active 3/1 2 min ago claude add-export-api .worktrees/add-export-api active 2/0 5 min ago codex improve-test-coverage .worktrees/improve-test-coverage provisioned 0/0 - claudeAhead/Behind列会显示该任务分支领先或落后基分支的提交数,是判断“代理是否基于最新代码”的关键指标。看到某个任务的 Behind 越来越多,就该执行同步操作了。
4.4 成果合并与工作区回收
假设三个代理都完成了任务,下一步就是逐个检查、同步并合并。
先看改动量,确认代理没把项目搞乱:
worktrunk task diff --stat fix-login-timeout确认改动符合预期后,同步基分支最新提交,然后合并:
worktrunk task sync fix-login-timeout --method rebase worktrunk task merge fix-login-timeout --method mergesync使用 rebase 方式,是把代理基于旧主干开发的提交重放到最新主干之上,提交历史更清晰;merge则保留任务分支的独立语义,适合想看到完整合并轨迹的团队。
合并成功后,Worktrunk 会自动切回基分支、删除任务分支、移除对应 worktree 目录,并把状态改为merged。回收干净:
worktrunk task cleanup fix-login-timeout三个任务流水线式走完,主分支的提交历史始终是干净的线性结构,每个任务都能独立回看和回滚。
5. 底层实现要点:从 Git 命令到可交付 CLI
5.1 与 Git 子进程交互的可靠性设计
Worktrunk 本质上是 Git 的客户端,所以与 Git 子进程的交互质量直接决定工具的稳定性。这块有几个设计细节值得展开。
第一,永远用--porcelain格式解析输出,而不是 grep 普通文本。git worktree list --porcelain、git status --porcelain、git branch --format这些专门为程序准备的输出格式,字段稳定、编码明确,避免本地化语言导致解析崩溃。
第二,不要手工拼接命令行参数。多用编程语言提供的命令行参数列表直接传参,不要把任务名直接拼进git checkout -b ...的字符串里。任务名里万一有个空格或者以-开头,拼接方式很容易导致参数解释错误,甚至执行了非预期操作。Worktrunk 一律使用参数数组方式调用 Git,从根上杜绝这类问题。
第三,对 Git 命令的退出码不做想当然的假设。比如git merge在有冲突时返回非零退出码,但有些版本的 Git 在特定场景下的行为会有差异。Worktrunk 捕获退出码后会结合 stderr 内容做二次判断,确认是真正的冲突还是环境问题,避免误报状态。
5.2 工作区状态的持久化与恢复
前面提到状态文件放在.git/worktrunk/state.json,这个选择的另一个好处是天然支持多终端操作。假设你在终端 A 创建了任务,终端 B 执行task list,只要状态文件是可靠的,两个终端看到的就是同一份信息。
状态文件的设计要保证崩溃恢复。每次状态变更都先写入临时文件再原子重命名,避免进程被中断时留下损坏的半截 JSON。数据量极小,这种简单方案比引入数据库轻量得多,也足够可靠。
此外,每个 Task 除了状态字段还要记录 worktree 的绝对路径。绝对路径在机器上移动仓库时可能会失效,所以 Worktrunk 在启动时会根据.git文件里的gitdir路径重新解析仓库根目录,再结合相对路径还原出正确的 worktree 位置,保证“仓库搬家不丢状态”。
5.3 并发安全的锁与串行化
多个终端同时执行 Worktrunk 命令是完全可能的:一个人在执行task new,另一个人在执行task merge,如果两个操作同时对同一个仓库跑git worktree add,Git 本身会报错。
Worktrunk 在.git/worktrunk/下维护一个文件锁,所有会改变状态的操作(new、sync、merge、cleanup)在执行前必须先获取锁,获取失败就提示等待或报错。读操作(list、diff)不占锁,可以随意并发执行。
这个锁用操作系统级的文件锁机制实现,进程异常退出时系统会自动释放,不会出现死锁。多用户同时操作同一仓库的场景少见,所以只对同机进程做互斥足够。
5.4 跨平台路径与 Shell 差异
开发工具如果只照顾 macOS 和 Linux,在真实团队里基本会立刻被 Windows 同事吐槽。Worktrunk 的跨平台处理集中在三个点:
- 路径统一用语言内置的 path 库拼接,绝不手写
/或\分隔。 - 调 Git 命令时显式指定可执行文件名,Windows 下会自动加上
.exe后缀。 - 启动 Agent 时用参数数组而非 shell 字符串,绕开 Windows 下 cmd 和 PowerShell 转义规则不一致的问题。
实测下来,Windows 上主要注意 Git for Windows 安装路径要提前加入 PATH,其它方面 Worktrunk 的行为与 Unix 平台完全一致。
6. 实测中的坑与应对
6.1 孤儿 Worktree 的清理问题
Worktrunk 用状态文件记录任务,但 Git 本身对 worktree 的记录是独立的。如果某个用户绕过工具手动删除了.worktrees/下的目录,Git 的元数据里还是会残留记录,git worktree list里会出现一个不存在的路径。
这种孤儿记录会阻塞以后在这个路径创建新 worktree。应对方式是执行git worktree prune,让 Git 清理失效引用。Worktrunk 在每次启动时会自动检查并执行清理,所以正常情况下用户感受不到这个问题。但自己手动操作 Git 目录时务必小心,别直接用rm -rf删 worktree 目录,要走worktrunk task cleanup或git worktree remove。
6.2 磁盘占用机制中的“假共享”
Git Worktree 共享对象库不假,commit 对象、blob 对象都只有一份。但每个 worktree 的工作目录是独立的,构建产物和依赖目录不会共享。三个 worktree 各跑一次pnpm install,node_modules就是三份;如果项目还用 webpack 之类的产生大量构建缓存,磁盘开销会成倍增长。
应对策略有两条:一是配置包管理器的全局缓存,比如 pnpm 的 content-addressable store 本身就支持跨目录去重,占用没有看起来那么吓人;二是把构建缓存目录(如.cache、dist、target)配置到仓库之外或设置 gitignore,让各 worktree 不复用这些瞬时产物。实测一个中等规模前端项目开五个 worktree,依赖加构建缓存总共增加不到两倍的仓库体积,可接受。
6.3 子模块与软链接的边界情况
仓库里如果有 Git Submodule,直接创建 worktree 后子模块目录通常是空的。需要在每个新 worktree 里执行git submodule update --init --recursive才能把子模块内容拉下来。Worktrunk 的配置文件里预留了post-create钩子,可以在创建任务工作区后自动执行这个命令。
软链接则相对简单,创建 worktree 后软链接目标如果是相对路径,会自动跟随目录结构;如果是绝对路径,每个 worktree 会指向同一目标。这既是便利也是风险——一个 worktree 里动了链接指向的共享文件,其他 worktree 也会受影响。团队如果重度使用绝对路径软链接,建议统一改成相对路径或在文档中注明不做跨 worktree 共享。
6.4 Agent 无头模式的环境变量透传
Agent 启动时不只是一条命令那么简单,它需要 API Key、模型名称、代理配置、远端服务地址等一系列环境变量。Worktrunk 在内部启动 Agent 时,默认继承当前 shell 的全部环境变量,保证和用户手动执行时行为一致。
有一个坑是某些 Agent CLI 会创建自己的配置文件,而配置文件的默认读取位置是用户主目录。如果多个代理实例并发运行,它们可能同时写同一个配置文件,导致配置互相覆盖。实测 Claude Code 在并发场景下偶发这种问题,目前的做法是给每个任务的 Agent 进程设置独立的CLAUDE_CONFIG_DIR环境变量,指向 worktree 下的隐藏目录,让各实例的配置互不可见。
这点其实很有启发:工具层面的隔离做得再好,Agent 壳层自身也会产生侧信道干扰。把可写的用户级配置也一并隔离,并行场景才会真正干净。
用下来最大的体会是,AI Agent 并行开发的瓶颈往往不在模型能力,而在工程化约束。Worktrunk 把 Git Worktree 这套底层的隔离能力变得对人和对 Agent 都可用,让“多个代理同时改一个仓库”从混乱状态变成可控流程。下一步我还在观察的方向是:在 merge 之前自动跑一轮 CI 校验,让不达标的任务分支直接被拦下来。如果这个闭环打通,并行 Agent 开发就可以从“多个代理同时干活”升级成“多个代理在自动化质量门禁下同时交付”。