ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

并行AI Agent开发利器:git worktree与Worktrunk工作流实践

并行AI Agent开发利器:git worktree与Worktrunk工作流实践 我最早被并行 AI Agent 开发逼疯是在一个不算大的 Node.js 服务仓库里同时跑三个任务一个修登录模块的竞态条件一个接支付回调还有一个重构日志中间件。当时我偷懒的方式很朴素——开了三个终端标签页每个里git checkout -b feature/xxx然后切来切去。结果不到半天就崩了Agent A 改了package-lock.jsonAgent B 构建时直接心态爆炸Agent C 顺手跑了个测试脚本把公共缓存目录清掉了最后提交时三个分支里混着彼此的工作产物review 的时候根本分不清哪个 diff 属于哪个任务。后来我把目光放到了我一直知道、但总觉得“没必要用”的git worktree上再配合我自己写的一个 CLI 工具Worktrunk把整套流程彻底重做了。这篇文章就聊聊为什么要给并行 AI Agent 工作流配 worktree 管理工具、Worktrunk 的设计逻辑是什么以及我把三个 Agent 塞进同一个仓库并行开发时总结出来的实操经验和坑。1. AI Agent 并行开发的痛点藏在一个“同一份工作区”里1.1 Agent 的工作方式天然不适合“一个目录多分支切换”人和 AI Agent 在并发任务上的差异远比很多人想象的大。人类开发者在一个仓库里开多个分支来回切换是可行的因为人知道“我现在切到 feature-a 分支工作区里残留的 feature-b 产物我要小心处理”这个判断力是基于全局上下文和经验的。AI Agent 没有这个意识。它被要求“修登录模块的 bug”就会开始读文件、改文件、跑测试、安装依赖、生成临时文件。它不会管你当前工作区是不是还残留着另一个任务的东西也不会在切换任务前主动帮你清理。让多个 Agent 共享同一个工作目录就像让三个厨师共用同一块案板切不同的菜——不是不能但串味是必然的。我自己实际遇到的典型事故包括Agent A 依赖安装时生成了新的package-lock.jsonAgent B 在执行构建前发现依赖不匹配自动帮你“修复”了锁文件导致 A 的改动全乱。两个 Agent 同时在自己的上下文里读取了src/config.ts但各自基于不同版本的缓存内容做了修改最后提交时互相覆盖。某个 Agent 在“探索性测试”阶段创建了一堆临时文件和 mock 数据目录另一个 Agent 误以为那是自己任务的产物顺手提交了上去。1.2 git worktree 提供了什么独立目录 共享对象库git worktree的核心能力一句话概括同一个仓库可以拥有多个工作目录每个工作目录独立检出、独立操作、互不干扰但共享同一个.git对象库和引用refs。它解决的不是 Git 存储层面的问题而是“物理隔离”层面的问题。核心原理是git worktree add会为每个新工作区创建独立的index、HEAD、和工作文件并将这些元数据记录在主仓库的.git/worktrees/name目录中。所有工作区共享同一个objects数据库所以提交、分支切换、对象查询都能在毫秒级完成不会因为目录数量增加而膨胀。用大白话说你在一个项目里开出几个互不干扰的“平行工作间”每个房间都有自己的文件状态和待办区但楼下共用同一个仓库货架东西放上去之后大家都能看到。1.3 原生命令够用但在多 Agent 场景下没人帮你“串起来”git worktree原生命令确实能做基本的事git worktree add ../feature-a -b feature-a git worktree list git worktree remove ../feature-a但在并行 AI Agent 工作流里原生命令的粒度太低了你记不住哪个 worktree 路径对应哪个 Agent 在跑的任务。你没法快速判断某块工作区是“进行中”“待评审”还是“可以清理”。你没法批量创建、批量清理、批量执行检查。你更没法把这个过程暴露给 Agent 自己的上下文——Agent 拿到一个孤零零的目录并不知道它在这个大工作流里处于什么位置。所以我设计了 Worktrunk一个把 worktree 管理抽象成“任务工作区管理”的 CLI。它的核心思路不是发明新的 Git 机制而是把 Git 原生的 worktree 能力包装成适合多人/多 Agent 协作的心智模型。2. Worktrunk 的设计逻辑把分支抽象成“可隔离的工作席位”2.1 数据模型任务、工作区、AgentWorktrunk 内部把一切都组织为三个实体Task任务一个可追踪的开发目标比如“修复登录竞态条件”“接入支付回调”。它映射到一条分支、一个工作目录、一组元数据。Worktree工作区任务在磁盘上的物理落点指的就是git worktree add创建的那个目录。Agent执行者负责该任务的 AI 工具。可以是 Codex CLI、Claude Code、本地自定义脚本甚至人类自己。它本身不是必要实体但记录下来之后你可以一眼看出“这个任务目前在谁手里”。对应到磁盘上的元数据我采用的是一个简单 JSON 文件存放在.git/worktrunk/state.json因为这样人类可读、脚本可解析未来接 Web UI 也不费劲。一个最小示例长这样{ version: 1, workspaces: [ { name: login-fix, branch: feature/login-fix, baseBranch: origin/main, path: .worktrunk/login-fix, status: in-progress, agent: codex, createdAt: 2025-06-10T10:00:0008:00, lastActiveAt: 2025-06-10T11:30:0008:00 } ] }这个模型奠定了后续所有命令的基础。因为有了“任务”这个层级Worktrunk 就不只是操作一堆无序的目录而是你可以问它“当前有哪些活跃任务Agent 是哪个哪些三天没动了”2.2 命令集的心智模型开、看、切、收在设计 CLI 命令时我一直提醒自己要克制。很多工具死在“什么命令都加”上包括我曾经写过的内部脚本最后命令数比功能数还多。Worktrunk 的命令集只需要围绕四个动作展开动作命令一句话解释开worktrunk create为任务创建独立工作区看worktrunk list查看所有工作区与状态切worktrunk focus把当前 shell 切入指定工作区收worktrunk close/worktrunk clean标记完成并回收工作区这套命令背后的状态机是未开始 → 进行中 → 待评审 → 已合并 → 待回收。每个工作区都有自己的状态list输出中会直接展示出来focus只允许聚焦到未结束的任务clean只会回收已经合并或明确标记完成的工作区。为什么这样设计因为只有把命令减少到四个方向使用者包括 Agent 自己才能在没有文档的情况下快速形成肌肉记忆。我自己实际用下来的感受是AI Agent 只需要知道create和focus人只需要知道list和clean角色边界非常清晰。2.3 为什么是 CLI而不是 IDE 插件或 GUI这个问题的答案一开始可能会让人觉得“逆潮流”。AI 编程时代大家都在做 GUI、做 IDE 集成为什么 Worktrunk 还是要做 CLI几个实际原因AI Agent 的交互入口本来就长在终端上。Codex CLI、Claude Code、Devin 这类工具的调用方式都是终端命令。CLI 可以被 Agent 直接调用和解析GUI 不行。脚本化、可组合。支持--json输出意味着可以轻松接进 CI、巡检脚本、定时任务。我要在没人在场的时候自动清理三天未动的 worktreeGUI 做不到。部署门槛低。一个二进制文件装到任意有 Git 的机器上就能跑不依赖图形环境在远程开发服务器上也能用。这个选择背后是务实CLI 是 Agent 生态的“最小公分母”。你的工具只要还在 CLI 层暴露能力就能被未来任何新的 Agent 工具直接复用。2.4 元数据放哪项目内、.git 目录回避两个极端元数据存放位置是个容易翻车的设计决策。如果存到工作区根目录比如worktrunk.json好处是可以在代码评审里看到坏处是它会被提交进仓库污染每个任务的 diff。如果存到用户级目录比如~/.worktrunk/state.json又没法保证多个仓库互不干扰。我的选择是存进.git/worktrunk/state.json。理由它天然属于仓库元数据不会污染工作目录和提交历史。它在git clone时不会被带过去但本机所有 worktree 都能访问到。删除仓库时一起被清理不会留垃圾。这个思路和 Git 自身的设计高度一致关于本仓库的元信息都应放在.git里。3. 安装和核心命令逐条拆解3.1 安装与前置条件Worktrunk 是一个单二进制 CLI安装方式取决于项目发布渠道。我当时是直接把编译后的二进制丢进$PATH后来正式化之后支持了常用的安装方式# 如果你用 Homebrew brew install worktrunk # 或者通过 npm 全局安装 npm install -g worktrunk/cli # 或者直接拉取官方发布的二进制 curl -sSL https://example.com/worktrunk/install.sh | bash前置条件很轻Git 版本建议 2.30 以上因为git worktree的若干交互修复集中在这个版本之后。如果你在 Windows 上用建议开启git config --global core.longpaths true这个坑后面专门讲。shell 建议 bash/zsh/fishPowerShell 也能用只是focus的 shell 集成需要额外配置。3.2 初始化worktrunk init进入一个已有的 Git 仓库目录执行cd ~/apps/my-service worktrunk init输出类似Initialized Worktrunk in /Users/me/apps/my-service Default worktree path: .worktrunk Agent base template: noneinit会做三件事检查当前目录是否是 Git 仓库。在.git/worktrunk/下创建元数据目录和默认state.json。读取你当前的 Git 配置写入默认基分支、默认工作区路径、Agent 模板等基础配置。这里有一个很反直觉的设计我不把 worktree 放到项目外的兄弟目录而是默认放在项目内的.worktrunk/workspace-name。理由是当你用相对路径在子目录里工作时编辑器、索引服务、文件监听器对路径的识别更稳定同时.gitignore里加一行.worktrunk/就能避免误提交。如果你更习惯放在项目外也可以改配置。3.3 创建任务工作区worktrunk createcreate是使用频次最高的命令也是参数最多的一个。我列举几个实用参数worktrunk create login-fix worktrunk create payment-callback --agent claude --base origin/main --template claude worktrunk create log-refactor --agent custom --path .worktrunk/log-svc参数作用说明--name/ 位置参数任务名也是默认工作区目录名--base基分支默认取仓库当前分支建议显式传origin/main--agent执行者支持codex、claude、custom影响模板注入--path自定义工作区路径默认.worktrunk/task-name--template模板名可为 Agent 注入特定的上下文文件--branch分支名默认feature/task-name执行示例$ worktrunk create payment-callback --agent claude --base origin/main --template claude ✔ Created workspace at .worktrunk/payment-callback Branch: feature/payment-callback Base: origin/main Agent: claude Template: claude内部逻辑是git worktree add -b feature/payment-callback .worktrunk/payment-callback origin/main然后把元数据写进.git/worktrunk/state.json。如果配置了模板还会在工作区里生成CLAUDE.md或AGENTS.md等上下文引导文件让 Agent 一进来就知道这个任务的目标、约束和验收标准。这个模板注入能力对 AI Agent 工作流格外关键。我的做法是每个任务的CLAUDE.md里明确写清楚“本工作区只处理支付回调相关代码禁止改动其他模块、禁止读取兄弟目录”。实际效果比在 prompt 里反复叮嘱好得多。3.4 状态总览worktrunk listlist是日常使用最多的巡检命令$ worktrunk list ╭──────────────────┬─────────────┬──────────────────────┬────────┬───────╮ │ Workspace │ Branch │ Status │ Agent │ Last │ ├──────────────────┼─────────────┼──────────────────────┼────────┼───────┤ │ login-fix │ feature/… │ in-progress │ codex │ 10:31 │ │ payment-callback │ feature/… │ in-progress │ claude │ 09:02 │ │ log-refactor │ feature/… │ ready-for-review │ custom │ 昨天 │ ╰──────────────────┴─────────────┴──────────────────────┴────────┴───────╯如果你要写脚本轮询list --json输出结构化数据更好用worktrunk list --json[ { name: login-fix, status: in-progress, branch: feature/login-fix, agent: codex, lastActiveAt: 2025-06-10T10:31:0008:00 } ]我编这个命令的时候最看重的是“一屏扫完”的体验。因为并行工作流的瓶颈通常不在操作而在“信息聚合”——你是不是能快速知道哪个任务卡住了、哪个 agent 还在跑、哪个可以收工了。3.5 切换worktrunk focus 与 shell 集成focus的设计初衷是“把当前终端切换到目标工作区并绑定对应上下文”实现方式比较特殊。因为 CLI 子进程不能修改父 shell 的$PWD我采用输出 shell 代码让用户eval的方式eval $(worktrunk focus payment-callback)执行后当前 shell 会cd到.worktrunk/payment-callback。导出$WORKTRUNK_ACTIVE、$WORKTRUNK_TASK环境变量。如果配置了模板会重新加载.worktrunrc之类的环境文件如果有。我实际用下来的体验是配合 tmux 或 zellij 尤其舒服。一个窗口一个 Agent每个窗口执行一次eval $(worktrunk focus xxx)窗口标题都会自动变成任务名再也不会找不到自己在哪。3.6 任务完成与回收worktrunk close / cleanclose标记任务完成worktrunk close login-fixclose不会删除工作区只是把状态改为completed防止你误操作。clean才真正回收worktrunk clean login-fix回收前 Worktrunk 会做四重检查工作区是否存在未提交变更或未推送提交。分支是否已经合并回基分支或者通过--force强制跳过。是否有其他进程在工作区内运行通过简单的 PID 探测。是否处于in-progress状态默认禁止清理进行中任务。只有在全部通过后才会执行git worktree remove并同步删除.git/worktrunk/state.json里的记录。这个安全策略帮我挡住过至少三次误操作。3.7 配置项与 shell 补全Worktrunk 的配置分两层用户级配置~/.config/worktrunk/config.toml存放默认 Agent 模板、默认基分支、是否自动开启提示符插件。仓库级配置.git/worktrunk/config.json存放当前仓库的 worktree 路径前缀、默认分支前缀等。# ~/.config/worktrunk/config.toml [defaults] base origin/main agent codex worktree_root .worktrunk [agent.codex] template codex [agent.claude] template claudeShell 补全支持 bash/zsh/fish安装时自动写入# zsh 示例 eval $(worktrunk completion zsh)4. 实战演练三个 Agent 并行开发一个 Web 服务4.1 场景设定仓库~/apps/pay-server这是一个 Node.js Web 服务当前main分支稳定在 v1.2.0。我同时收到三个任务login-fix修复登录模块的竞态条件派给 Codex CLI。payment-callback接入支付回调验签派给 Claude Code。log-refactor重构日志中间件我自己写或者派给本地脚本 Agent。4.2 从 0 到 1 建三个工作区cd ~/apps/pay-server worktrunk init worktrunk create login-fix --agent codex --base origin/main --template codex worktrunk create payment-callback --agent claude --base origin/main --template claude worktrunk create log-refactor --agent custom --base origin/main --template agents此时目录结构变为pay-server/ ├── .git/ │ └── worktrunk/ │ ├── state.json │ └── config.json ├── .worktrunk/ │ ├── login-fix/ # Codex CLI 的私有工作区 │ ├── payment-callback/ # Claude Code 的私有工作区 │ └── log-refactor/ # 自定义 Agent 的工作区 ├── src/ ├── package.json └── ...每个工作区里都有独立的node_modules或者由包管理器以 symlink 方式复用取决于你的设置有独立的package-lock.json还有各自的CLAUDE.md/AGENTS.md模板文件。三个 Agent 从这一刻起就彻底分居了。4.3 三个 Agent 并行开工我作为调度者在做什么我开了三个 tmux 窗口窗口一eval $(worktrunk focus login-fix) codexCodex CLI 进来后会自动读取工作区里的AGENTS.md知道自己的目标范围默认不会跨目录扫描兄弟工作区。窗口二eval $(worktrunk focus payment-callback) claudeClaude Code 同样被模板约束只负责支付回调相关目录。窗口三我自己写日志中间件。在实际开发过程中我会隔一段时间执行一次worktrunk list看状态有没有卡住。Agent 跑完一个阶段后通常会自己在工作区里提交 commit。我只需要看到某个工作区的分支领先 main 若干 commit就知道这部分可以进入 review 了。一个很实用的巡检脚本思路可以每天定时跑一次#!/usr/bin/env bash # 找出超过24小时没有活动、但状态仍是 in-progress 的任务 worktrunk list --json | jq -r .[] | select(.status in-progress) | select((now - (.lastActiveAt | fromdateiso8601)) 86400) | .name | while read name; do echo [提醒] 任务 $name 已超过24小时未更新 done这套思路的本质是把 worktree 的状态变成可编程的数据然后让脚本替人盯梢。4.4 合并和清理每个 Agent 结束时我做了什么当 Agent 通知任务完成我对待login-fix的流程是eval $(worktrunk focus login-fix) # 拉取最新 main 并 rebase确保可以干净合并 git fetch origin main git rebase origin/main # 跑一遍测试 npm test # 确认没问题后切回主工作区并合并 cd ~/apps/pay-server git checkout main git merge --no-ff feature/login-fix -m feat: 修复登录竞态条件 worktrunk close login-fix worktrunk clean login-fixclean执行后.worktrunk/login-fix目录被移除state 里对应记录消失整个流程无残留。三个任务也可以有先后地合并互不影响。即使 payment-callback 还没做完login-fix 也可以先发布上线——这在传统单工作区模式下需要小心翼翼在 worktree 模式下是自然操作。4.5 用 --json 把工作区状态接进 CI 和 Agent 上下文有状态的地方就能做自动化。Worktrunk 的--json输出让它可以很自然地和 CI 集成在 pre-merge 检查时要求对应 worktree 的测试全部通过才允许合并。在定时巡检时把list --json的结果推送到企业 IM 机器人提醒过期任务。在部署流水线里用脚本筛选出所有状态为ready-for-review的工作区批量发起 PR。对于 Agent 本身也可以让它们通过 CLI 感知全局状态。我在自己的 Agent 工具链里加了一个系统命令别名wt直接输出当前工作区的任务描述让 Agent 每次决策前都能重新确认“自己在为哪个目标工作”。5. 实际使用中踩过的坑5.1 clean 误删未提交变更差一点丢掉半天的成果第一次做clean时我以为工作区分支已经合并了就把login-fix干净利落地删了。结果等想起来里面还有一个没推送的 debug 脚本时已经太晚了。查了.git/fsck才把 dangling commit 捞回来。后来我给clean加了三层保护默认只允许清理状态为completed或分支已合并的工作区。执行前做一次git status --porcelain检查有未提交变更就拒绝。即使是强制清理--force也会先把工作区历史压缩归档到.git/worktrunk/archive/保留 7 天。这次教训让我深刻意识到任何“批量回收”类命令都必须默认开启“后悔药”模式。人很容易过度相信自己做过的检查而工具应该替你挡住这一层。5.2 Windows 下的长路径问题是最大的绊脚石git worktree本身在 Windows 上可用但路径嵌套一旦变深就会撞上 Windows 传统的 260 字符路径限制。由于 Worktrunk 的工作区默认放在项目内部比如C:\Users\me\repo\.worktrunk\some-long-task-name很容易触发。解决办法是提前设置git config --global core.longpaths true如果问题依旧可以自定义更短的工作区路径前缀worktrunk config set workspace.root C:\\wt这样每个工作区就变成C:\wt\login-fix路径短一大截基本能绕开限制。5.3 子模块和 monorepo 的兼容细节如果你所在仓库使用了git submodule直接对父仓库建 worktree 时子模块的版本指针会跟着切换但子模块本身的.git目录不会自动跟随容易造成子模块目录状态错乱。我目前实践出来的两个方案方案 A推荐把子模块当作独立仓库为每个子模块单独创建 worktree。父仓库的 worktree 只负责锁定子模块的提交哈希。方案 B在每次worktrunk create后手动执行git submodule update --init --recursive确保子模块文件完整。对于 monorepo比如 pnpm workspace / turborepo我的经验是先在整个仓库建 worktree然后在工作区里只对packages/内需要修改的包执行包管理器命令。不要尝试在每个包里单独建 worktree那样会让依赖解析和本地链接变得极其痛苦。5.4 AI Agent 对“工作目录”的适配差异比我想象的大不同 Agent 对工作目录的认知方式差异是实际使用中隐藏最深的坑。Codex CLI偏向以当前目录作为根读取根目录的AGENTS.md和项目配置相对守规矩。Claude Code的 memory 和 MCP 配置是全局的如果不加约束它会跨工作区读取自己的记忆文件导致任务 A 里记住的上下文被带到任务 B。某些本地 Agent 脚本会把“当前目录”当作唯一感知对象很容易忽略工作区外的大仓库全局状态。我的应对方案是模板注入。每个 Agent 工作区生成专属的上下文文件明确“只处理本目录内与任务相关的范围禁止跨界读取”。环境变量隔离。在focus时导出WORKTRUNK_TASK_MEM_DIR之类变量Agent 的记忆目录按任务名隔离。物理隔离优先。不要指望 Agent 自觉直接把兄弟工作区设成它无权限访问的位置。一句话总结Agent 的自我约束能力是有限的工具的物理隔离能力是无限的。能用物理隔离解决的问题别指望 prompt 解决。6. 我认为 Worktrunk 下一步该往哪里长6.1 并发锁与无人值守目前clean对“进程是否在工作区里运行”的检测很粗糙扫描打开文件句柄或 PID 列表。下一步应该引入更严格的锁机制Agent 开工时在工作区里写一个lock文件释放时删除clean看到锁就拒绝操作。只有这样才能在无人值守的夜间任务中放心地做自动回收。6.2 与问题任务系统的联动Worktrunk 现在并不知道当前这个任务是来自 issue 还是自行发起的。如果能和 GitHub Issue、Jira、飞书/Lark 多维表格之类的任务系统打通worktrunk create #123 login-fix就能自动拉取 issue 描述、生成分支、创建 PR、回写状态这会大幅降低团队协作成本。核心设计只需要保留--json的注入点剩下的是各个平台的适配器。6.3 hook 机制参考 Git hookWorktrunk 也可以支持on-create、on-focus、on-close、on-clean四类 hook。我的设想是on-create时自动向 Agent 模板里注入任务明细。on-merge时触发 CI。on-clean时通知任务系统“工作区已回收”。这类自动化会让“并行 Agent 工作流”真正变成一个可观测、可治理的管道而不是几个人各自开着终端乱跑。6.4 要不要做 Web UI / TUI我的答案目前是不做或者说暂时不做。原因不是技术难度而是受众问题。单人使用场景下CLI 已经是效率天花板多人大规模协作时Web UI 才有意义。到了 worktree 数量超过 20 个、你需要让不熟悉命令行的人也能看到“哪些任务堵了”的时候Web 面板才会成为刚需。但核心数据结构已经是 JSON未来接 UI 只是加一层壳的事。我一直认为先做好数据层的可编程性UI 只是顺水推舟。最后分享一个我实际养成的小习惯在每个 Agent 工作区里第一时间把分支名和任务描述写进AGENTS.md的开头并把WORKTRUNK_ACTIVE变量显示在终端提示符里。这样不管是人类还是 Agent在任何时刻看一眼提示符就知道自己身处哪个平行世界而不是靠记忆和猜。Worktrunk 帮我解决的说到底不只是“目录隔离”而是“上下文隔离”——在 AI Agent 并行开发成为常态的今天这件事比什么都值得认真处理。
返回列表