
1. 一串并行 Agent 跑在同一个 Git 仓库上迟早要在工作区打架我最近的一个项目里同时挂着三个 AI Agent 在同一个仓库上干活Codex CLI 负责改支付流程Claude Code CLI 负责修启动崩溃还有一个文档 Agent 在补 API 说明。听起来很美好实际跑了不到两天就翻车了。翻车的第一个信号是Agent A 写到一半的未提交文件被 Agent B 的一次git checkout直接覆盖了。Git 倒是没有真的把对方的文件删掉只是在切分支的时候报了Your local changes would be overwritten然后 Arge 进程直接卡死。这事在单 Agent 场景里好处理你手动 stash 一下就行但在并行场景里背后没有人盯着Agent 自己处理不了这种工作区被别人占用的状态。第二个信号更隐蔽因为所有 Agent 共享同一个工作目录它们看到的代码状态是互相污染过的。文档 Agent 提交了一份描述未来支付接口的文档但其实支付接口那一半代码是 Codex CLI 在另一个分支上还没合入的内容。于是整个文档失真成了给下一轮 Agent 的错误提示。Git 的分支体系能隔离已提交的逻辑变更但它隔离不了未提交的工作区文件而这恰恰是 Agent 最常用的东西——一个 Agent 的运行轨迹本质上就是一连串创建文件、修改文件、跑测试、再修改的流式操作。这个问题的解法其实早就存在就是 Git Worktree。git worktree add可以让你在同一个仓库上挂出多个物理隔离的工作目录每个目录对应独立的分支它们共享.git/objects对象库但工作区、索引、HEAD 完全分开。对应到 Agent 场景就是每个 Agent 一个独立工位谁也不会碰谁的未提交文件。但裸的git worktree有个问题它只是 Git 的底层原语命令里没有任务这个概念。分支叫什么、目录放在哪、base 分支是哪个、什么时候该回收全都靠人脑去记。于是我自己写了一个命令行工具叫 Worktrunk专门把这些规则固化下来以并行的 AI Agent 任务为第一视角把创建、切换、同步、合入、回收这一整条生命周期封装成几个简单命令。这篇文章就讲讲这个工具的思路、用法以及我在多 Agent 并行场景里踩过的坑。2. Worktrunk 的设计骨架把 Git Worktree 变成 Agent 的独立工位2.1 从任务生命周期出发而不是从 Git 原语出发Git 原生命令的设计出发点是仓库操作不是任务管理。面对一个 Agent你真正想要的交互是这样的我想要一块独立的工作区去完成某个任务我要知道我现在在哪个任务上我要同步基础分支的最新改动我做完了需要合回去然后把这间工位清掉Worktrunk 的命令体系完全按这个叙事来兜。初始化一个 Agent 工作区根的目录然后每个任务占用一个目录和一个分支任务结束自动回收。这里有四个核心命令# 初始化把已有仓库挂载到 agent 工作区根目录 worktrunk init --root ~/trunk-agents --repo ~/code/myapp # 创建任务创建完会输出新 worktree 的路径 worktrunk create \ --name feat/payment-form \ --base main \ --label 接入支付表单提交逻辑 \ --agent codex # 查看所有任务的当前状态 worktrunk list # 把 base 分支的最新改动同步进当前任务 worktrunk sync # 合入 base 分支并回收任务 worktree worktrunk merge --no-ff worktrunk prune --merged-only你可能注意到了这里刻意去掉了过多暴露 Git 语义的措辞。sync内部可能做的是git merge origin/main但使用者不需要关心这些。对 Agent 来说命令越是收敛越不容易在工具调用的环节出错。2.2 一个 Agent 一个目录是一道很低成本的隔离git worktree add相比git clone最大的优势是共享对象库。你 clone 一个 800MB 的仓库每个 Agent 就得扛一份 800MB而 worktree 方式下对象库只有一份每个 worktree 目录里只有工作区文件加一个指向.git的元数据文件。代价是磁盘上每个 worktree 仍会有一份完整的工作区文件以及一次 checkout 带来的索引文件。不过在现代机器上这通常不是瓶颈真正有价值的回报是Agent 之间彻底不共享未提交状态也不共享构建目录里那些半成品产物。如果你写代码时习惯开npm run dev或者 watch 模式你会发现每个 worktree 里的进程互相完全隔离不会再出现另一个 Agent 直接把你的 dev server 搞挂的灵异事件。2.3 Worktrunk 在背后维护的任务清单Worktrunk 会在仓库的.worktrunk/目录下维护一份任务清单每个任务一个 YAML 文件记录分支名、Agent 类型、影响到的文件集合、创建时间、最近同步时间。这份任务清单解决的核心问题是让命令有状态worktrunk list不只是一个 Git 视图还是一个任务视图。表格输出大概是这样的TASK NAME BRANCH PATH HEAD STATUS feat/payment-form feat/payment-form ./feat/payment-form a1b2c3d clean fix/crash-on-start fix/crash-on-start ./fix/crash-on-start e4f5a6b sync-needed docs/api-ref docs/api-ref ./docs/api-ref c7d8e9f mergedSTATUS列的内容来自 Worktrunk 对任务状态的推断分支是否落后 base、工作区是否 dirty、是否已经合并过。Agent 判断我能不能开始干活只看这一列就够了不用自己去跑一堆 Git 命令。2.4 对 Agent 友好的机器可读输出这里有一个细节很多人都容易忽略AI Agent 调用 CLI 工具时解析输出流的鲁棒性远比你想象的要差。彩色的表格、loading 动画、交互式确认提示都会让 Agent 的解析逻辑崩溃。所以 Worktrunk 所有命令都支持一个--porcelain模式输出稳定的、不可变结构的纯文本专门给 Agent 消费。worktrunk list --porcelain # output: # taskfeat/payment-form branchfeat/payment-form path./feat/payment-form statusclean # taskfix/crash-on-start branchfix/crash-on-start path./fix/crash-on-start statussync-needed我自己的经验是凡是准备给 Agent 用的 CLI都要默认无颜色、无交互、无花哨效果。这一点在把 Worktrunk 接给 Codex CLI 或 Claude Code CLI 的时候格外重要后面会展开讲。3. 把 Worktrunk 接进 Codex CLI / Claude Code CLI 的完整流程3.1 安装与初始化Worktrunk 是编译成单个二进制的 Go 程序装好之后放到 PATH 里即可没有运行时依赖。首次使用只需要两步拿到仓库路径然后确定你的 Agent 工作区放哪。考虑到很多 Agent 进程的工作目录没法随便改我建议把 Agent 工作区根目录建在仓库目录之外比如~/trunk-agents/repo-name/task-name。这样你后面运行 Agent 时直接以 task 目录作为它的启动目录仓库本身不会被 Agent 产生的一堆临时文件污染。# ~/trunk-agents/myapp/.worktrunk/config.yaml workspace: root: /home/user/trunk-agents/myapp repo: /home/user/code/myapp base_branch: main sync: auto: true validate: - npm run lint - npm test merge: mode: no-ff prune: keep_latest: 10 merged_only: true max_age_days: 7配置里有几个值得解释的点。sync.validate是 Worktrunk 在每次同步 base 分支改动后自动执行的验证命令列表按顺序跑全部通过才算一次成功的 sync。merge.mode固定为no-ff因为 Agent 产出的 commit 往往特别碎用--no-ff保留一个明确的合并提交事后回溯哪个 Agent 干了什么非常有用。prune里的三条规则是回收策略后面会展开。3.2 接入 Agent 的三种方式接入方式取决于你用的是哪种 Agent harness。方式一是目录注入在启动 Agent 进程之前先用worktrunk create拿到任务目录然后把cwd设到那里。这种方式最通用Codex CLI、Claude Code CLI 都适用# 启动一个 Codex CLI 对话目录落在任务 worktree 内 codex --cd ~/trunk-agents/myapp/feat/payment-form方式二是工具暴露如果你用的是支持自定义工具的 Agent 框架可以直接把worktrunk封装成一个工具函数让 Agent 在执行任务的过程中自己去创建和管理 worktree。注意工具描述要写得足够直白比如Create a new isolated worktree for a task. Returns file path. Use before any coding work.。方式三是壳命令包装写一个小脚本把create - run agent - sync - merge - prune整条流水线串起来。这是我最常用的方式适合你希望无人值守地跑并行 Agent 的场景#!/usr/bin/env bash set -euo pipefail TASK_DIR$(worktrunk create --name $1 --base main --porcelain) AGENT_DIR$TASK_DIR codex --cd $TASK_DIR $ worktrunk sync --path $TASK_DIR worktrunk merge --path $TASK_DIR --no-ff worktrunk prune --merged-only配一个任务看板三个 Agent 的当前状态一目了然。3.3 三个 Agent 并行跑的实际状态下面用一个具体例子展示并行效果。基础分支main三个 Agent 同时开工Agent任务分支工作目录状态Codex CLI接入支付表单feat/payment-form~/trunk-agents/myapp/feat/payment-formclean, readyClaude Code CLI修复启动崩溃fix/crash-on-start~/trunk-agents/myapp/fix/crash-on-startsync-needed文档 Agent补 API 文档docs/api-ref~/trunk-agents/myapp/docs/api-refmerged, 已回收文档 Agent 先完成了任务worktrunk merge把内容合回main之后自动回收了目录。另外两个 Agent 虽然没有感知到目录变化但当它们执行worktrunk sync时就能拿到文档 Agent 刚合入的最新提交。整个过程中没有一个人手动切换过分支也没有一次local changes would be overwritten的报错。3.4 Agent 提交历史碎的问题一个值得单独拿出来说的点是Agent 的提交习惯和人不一样它可能在 30 秒内产生十几条fix typo、refactor、wip这样的提交。这些提交在 merge 到main之前如果不做整理会严重污染主分支历史。Worktrunk 在merge之前默认会提示你做两件事第一如果任务分支上提交数量大于某个阈值默认 10 条建议先让 Agent 自己执行一次交互式变基合并第二如果任务的提交信息里包含wip或tmp关键字merge 流程会警告一次。Agent 写代码很强但让它给提交信息起名未必符合团队规范这个校验步骤算是给历史卫生兜个底。4. 多 Agent 并行时的基线同步与冲突处理策略4.1 基线漂移并行场景真正的大魔王多 Agent 并行时最麻烦的问题不是目录污染而是基线漂移。想象一下这个序列Agent A 从main基于 commit c1 切出分支开始改支付模块Agent B 从main基于同一个 c1 切出分支改启动逻辑并且很快改完合回去了main变成 c2Agent A 继续在自己的分支上干活但它的整个上下文还停留在 c1问题在于Agent A 不知道代码已经往前走了一步。如果它正好改了启动逻辑附近的东西等它合入时大概率会产生冲突更糟糕的是如果启动逻辑的接口签名被 Agent B 改了Agent A 的所有调用点都会编译失败而 Agent A 完全意识不到。Worktrunk 的解法是把sync变成一种强制的、可验证的节奏。配置里sync.auto: true时Worktrunk 会在create之后自动执行一次 sync保证 Agent 的初始状态就是最新的之后每次 Agent 调用worktrunk sync除了合并 base 分支的改动还会把sync.validate里的命令全部跑一遍。只有验证通过sync 才算成功否则任务状态会标记为sync-failedAgent 需要先解决验证问题再继续。4.2 为什么默认用 merge --no-ff而不是 rebase在多人协作的 Git 工作流里rebase经常被推荐因为历史是线性的。但 Agent 场景下我强烈建议merge --no-ff。原因是两方面的。首先Agent 的提交历史通常很碎如果你让它 rebase每一条碎提交都要单独解决一次冲突这对 Agent 来说是巨大的上下文切换成本而 merge 时冲突只需要解决一次。其次merge --no-ff 会保留一个清晰的任务合并提交这正好成为 Worktrunk 判别任务是否已经合入的依据——prune --merged-only只需要检查任务分支头是否已经是某个 merge 提交的祖先判断成本极低。唯一需要提醒的是merge 和 rebase 的最终代码内容是可以做到一致的team 的 code review 流程并不会因此受影响。我后来把 Worktrunk 的 merge 策略在内部文档里写死成了一律 no-ff禁止 rebase省掉了大量 Agent 重放提交引发的冲突。4.3 冲突的三道防线虽然 worktree 隔离了工作区但隔离不了代码逻辑上的所有权——两个 Agent 同时改同一个文件最终合入时还是会产生冲突。应对冲突我总结了三道防线第二道和第三道是 Worktrunk 特有的。第一道防线是模块边界规划。创建任务时让 Agent 声明它计划影响的文件集合Worktrunk 记录到任务 YAML 里# .worktrunk/tasks/feat-payment-form.yaml name: feat/payment-form agent: codex files: - src/** - api/** overlaps: - task: fix/crash-on-start files: [src/main.ts]当两个同时存在的任务声明了重叠的文件集合worktrunk list会在状态栏里直接打出overlap-warning。拿到警告的 Agent 可以主动错开边界把重叠的那一个文件交给对方处理。第二道防线是同步冲突前置。Worktrunk 的 sync 不是盲目 merge它会先检查这个任务分支是否落后于 base以及如果现在 merge哪些文件会产生冲突。如果预测到冲突sync 会拒绝执行并输出冲突文件清单让 Agent 决定是手动解决还是放弃本次同步。这比等到 merge 阶段才爆冲突要友好得多。第三道防线是Agent 自己解决冲突。当冲突真的发生后Worktrunk 不做任何智能合并它只负责把冲突文件列表交给当前 Agent由 Agent 在完整上下文里解决。你可能觉得这有点弱但实测下来让 Agent 自己理解冲突代码的语义再解决成功率远高于任何外部脚本。工具的核心价值是制造有序的上下文让 Agent 自主决策而不是代替 Agent 做决策。4.4 自动化的合入门闸合入前 Worktrunk 会执行一系列门闸检查sync.validate的全部命令、是否有未提交的修改、是否落后 base 分支、任务声明的文件集合里是否出现了未预期的文件改动。任何一项不通过merge 会中止并明确报出原因。这样配置完我可以在晚上睡觉前挂上三个 Agent 的任务早上起来只看一条总结日志就能知道昨晚哪个 Agent 爆了、为什么爆的、卡在哪一步。这是我个人认为 Worktrunk 最值钱的一部分——它不是加快了 Agent 写代码的速度而是让多个 Agent 并行协作这件事变得可以信任。5. 真实环境里的坑与排查链路5.1 Detached HEAD 陷阱第一次跑 Worktrunk 时我发现有个任务目录的worktrunk list状态异常分支列为空HEAD 显示的是一个散列的 SHA。查了一下日志是这个 Agent 在执行过程中跑了一个git checkout commit-sha的操作直接把工作区切到了裸 SHA 上于是整个 worktree 进入了detached HEAD状态。后续 Agent 的提交全都挂在半空中没有落在任何分支上prune --merged-only也永远找不到这个任务。排查链路是这样的worktrunk list --porcelain看到branch为空git -C task-dir status输出HEAD detached at a1b2c3d确认 Agent 日志里确实出现了裸 SHA 的 checkout 指令修复方法是在 Worktrunk 里加了一个repair命令它会读取.worktrunk/tasks/name.yaml里的任务分支名把 HEAD 重新指回分支同时保留当前工作区所有未提交内容。顺便说一句我在这一步之后给所有 Agent 的工具描述里加了一条约束不允许直接执行git checkout sha只允许用 Worktrunk 封装的命令。5.2 Worktree 元数据残留另一个高频问题是任务目录被手动删除后.git/worktrees/下会残留元数据。症状是重新创建一个同名分支时报错fatal: xxx is already used by a worktree。排查链路先看git worktree list --porcelain发现有一条记录的worktree /path/xxx路径早已不存在ls .git/worktrees确认清单里还有这个名字的目录跑git worktree prune清掉这些 stale 元数据Worktrunk 再做一层自己的任务清单清理因为.worktrunk/tasks/里也可能残留对应的 YAML这套先 git 层清理、再 worktrunk 层清理的双保险后来被集成进了worktrunk prune --all命令里。每当我发现worktrunk list出现幽灵任务时第一反应就是先跑这条命令。5.3 并发创建任务的竞态并行 Agent 一把梭的时候两个 Agent 几乎同时发起worktrunk create都用了同一个任务名称的资源。Git 层面对同名分支的创建不是原子的两个进程可能在检查分支存在性和实际创建之间产生竞态比较幸运的情况是其中一个创建失败并报错不幸运的情况是分支被覆写。排查链路查看.worktrunk/locks/目录发现没有锁文件残留确认两个 Agent 的日志时间戳在毫秒级重合验证git branch --list里分支 ref 的下游对象已经从最初的 commit 变成了另一个 commitWorktrunk 的解法是在创建任务前获取一个基于任务的锁文件O_EXCL创建拿不到锁的进程进入等待重试。代码其实不复杂但放在裸 Git 工作流里这种并发的细节很不容易被注意到。我现在所有分配任务的入口都经过 Worktrunk就是为了不再手工处理这种竞态。5.4 Windows 路径与文件系统差异如果你在 Windows 上跑 Worktrunk有几个坑需要提前知道。首先是路径大小写问题NTFS 默认大小写不敏感两个 Agent 一个生成src/Utils.ts一个生成src/utils.ts在文件系统层面会被当作同一个文件但 Git 对这两个路径的追踪是严格区分的。解决方式是创建任务时校验文件路径发现存在大小写碰撞直接报错不允许任务进行。其次是路径含中文或空格时很多 Agent 的 shell 解析会出问题。我的建议是工作区根目录统一用纯 ASCII 路径worktrunk create --name只允许小写字母、数字和连字符。这个限制一开始可能觉得死板但真的省掉了很多转义问题。再说一下文件监听。当一个仓库上挂着 5 个以上 worktree 时每个目录都要被 editor、test runner、build watcher 同时监听。Linux 上你会遇到ENOSPC: System limit for number of file watchers reached排查方式是cat /proc/sys/fs/inotify/max_user_watches解决方式是调高这个上限或者减少同时活跃的 worktree 数量。Worktrunk 的prune.max_age_days在这里才体现价值——它强制让不活跃任务自动过期避免 worktree 无限堆积。5.5 CI 与构建缓存的漂移最后一个坑比较隐蔽各个 worktree 因为各自检查出了代码构建产物也各自独立生成磁盘占用会成倍增长。更隐蔽的是有些构建工具比如使用build/目录的 Rust workspace会在多个 worktree 间共享部分缓存导致 A 任务的构建读到了 B 任务的中间产物测试结果随机性飙升。排查这类问题我会先看构建日志里的产物路径确认它们是否被外部变量污染然后在 Worktrunk 的配置里把每个任务的构建目录统一指向仓库外的临时目录task_environment: CARGO_TARGET_DIR: /tmp/worktrunk-targets/{{ task_name }}{{ task_name }}是 Worktrunk 支持的模板变量会在每个命令执行前注入到环境变量里。这样既保证了隔离又方便统一清理。排查这种坑的核心思路是先假设环境里有什么东西是各任务共享的再逐个排除。worktree 隔离了工作区但没有隔离环境这是很多人容易忽略的一层。6. 顺着 Worktrunk 继续往下做的三个扩展方向6.1 把任务生命周期接进自动合并流水线Worktrunk 目前是手动触发 merge。我在内部已经开始尝试把它接进一个简单的流水线Agent 完成任务后自动打一个 merge 请求MR 标题里带着任务名流水线先跑 Worktrunk 内置的validate和merge --no-ff合入后自动prune --merged-only。整个过程对 Agent 无感对人也基本无感。如果你想做可以在 CI 里加一个 job监听.worktrunk/events/下新增的task-completed.json文件以此作为触发信号。6.2 任务类型模板不同任务其实需要不同的验证策略。UI 改动要跑视觉回归和 lint后端性能修复要跑 benchmark文档任务只需要检查 link 是否有效。在任务 YAML 里加一个template字段指向.worktrunk/templates/下的脚本create时按模板自动生成对应的validate命令和工具描述。这个改动很小但对收窄 Agent 的行为非常有帮助它等于给角色定了型。6.3 磁盘水位和清理策略的自动化worktree 无限增长是所有人在长期使用后必然会遇到的问题。除了max_age_days和keep_latest之外加一个磁盘水位的硬性控制当workspace.root所在分区磁盘使用率超过 85% 时自动按最久未同步顺序回收任务且每次回收前把任务状态快照保存到.worktrunk/archive/里。这样即使任务被回收后续也能恢复出它当时的代码状态和日志。最后再分享一个我自己的日常用法我把worktrunk list --porcelain的输出塞进了 shell 的 prompt 右侧栏里每次敲命令都能看到当前一共有几个活跃 Agent、哪个任务需要同步、哪个任务已经合并完可以清理。这个习惯帮我在多 Agent 并行期间始终保持心里有数的状态比任何花哨的看板都直接。跑 Agent 这件事说到底不是跑得越快越好而是你随时能回答出现在到底发生了什么这个最基本的问题。