多个代码智能体不能安全地共享同一个工作目录,文件覆盖只是最直观的问题,依赖安装、格式化和未提交状态同样会互相干扰。本文用 Git Worktree 为每个任务建立独立工作区,并把创建、验收、合并和失败回收串成一条可执行的流水线。
一、先明确隔离边界
假设三个智能体同时处理接口重构、依赖升级和测试补全。如果它们共用一个 checkout,即使各自操作不同文件,也可能同时改写锁文件、生成代码或执行清理命令。一个智能体运行 git reset --hard,甚至会直接抹掉另一个智能体尚未提交的结果。
Git Worktree 允许一个仓库同时挂载多个工作目录,每个目录拥有独立的工作区和索引,并关联不同分支;对象数据库仍然共享,因此无需完整复制仓库。
| 对象 | 是否共享 | 实践建议 |
|---|---|---|
| Git 对象数据库 | 是 | 可节省重复克隆和对象存储 |
| 工作区文件与索引 | 否 | 每个任务使用单独分支和目录 |
node_modules、.venv | 否 | 放在各自工作区,不使用软链接共享 |
| 下载缓存 | 可以 | 共享 npm、pip、Go 等只读或并发安全缓存 |
| 合并入口 | 不应并行 | 由单一协调器串行写入目标分支 |
Worktree 解决的是文件和索引隔离,不会自动解决逻辑冲突。两个智能体分别提交了可工作的改动,合并后仍可能破坏行为,因此后续必须有集成验收阶段。
二、为任务创建分支和工作区
分支名最好同时携带任务编号和简短语义,例如 agent/184-user-cache。不要只使用智能体名称,因为智能体实例会被重复调度;任务编号才是稳定的追踪键。目录名可以与分支后半部分一致。
下面的脚本从指定基线创建分支和 Worktree。默认把工作区放在主仓库同级的 <仓库名>-worktrees 目录中:
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "用法: $0 <任务编号> <短名称> [基线分支]" >&2
exit 2
fi
task_id="$1"
slug="$2"
base="${3:-main}"
if [[ ! "$task_id" =~ ^[0-9]+$ || ! "$slug" =~ ^[a-z0-9-]+$ ]]; then
echo "任务编号必须是数字,短名称只能包含小写字母、数字和短横线" >&2
exit 2
fi
root="$(git rev-parse --show-toplevel)"
repo_name="$(basename "$root")"
worktree_home="${WORKTREE_HOME:-$(dirname "$root")/${repo_name}-worktrees}"
branch="agent/${task_id}-${slug}"
path="${worktree_home}/${task_id}-${slug}"
git rev-parse --verify "${base}^{commit}" >/dev/null
git check-ref-format --branch "$branch" >/dev/null
if git show-ref --verify --quiet "refs/heads/$branch"; then
echo "分支已存在: $branch" >&2
exit 1
fi
if [[ -e "$path" ]]; then
echo "路径已存在: $path" >&2
exit 1
fi
mkdir -p "$worktree_home"
git worktree add -b "$branch" "$path" "$base"
printf 'branch=%s\npath=%s\n' "$branch" "$path"
将其保存为 scripts/wt-create.sh 并执行 chmod +x scripts/wt-create.sh,之后可以运行:
./scripts/wt-create.sh 184 user-cache main
创建完成后,把工作区路径、分支名、基线提交 SHA 和验收命令一起交给智能体。基线 SHA 很重要:它能说明智能体究竟基于哪个版本工作,而不是含糊地说“基于 main”。
三、复用缓存,而不是复用可变依赖
每个 Worktree 都需要自己的依赖目录。直接在多个工作区之间共享 node_modules 或 .venv,可能让安装、卸载和原生扩展编译互相污染。更稳妥的做法是共享下载缓存,但在工作区内独立安装依赖。
下面的命令包装器为常见工具设置仓库外缓存,然后执行传入命令:
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -eq 0 ]]; then
echo "用法: $0 <命令> [参数...]" >&2
exit 2
fi
cache_root="${AGENT_CACHE_HOME:-$HOME/.cache/code-agents}"
mkdir -p "$cache_root/npm" "$cache_root/pip" "$cache_root/go-build" "$cache_root/go-mod"
export npm_config_cache="$cache_root/npm"
export PIP_CACHE_DIR="$cache_root/pip"
export GOCACHE="$cache_root/go-build"
export GOMODCACHE="$cache_root/go-mod"
exec "$@"
例如在 Node.js 项目中运行 ./scripts/agent-env.sh npm ci。缓存只减少重复下载,锁文件仍应由版本控制约束。智能体如果修改了 package-lock.json、go.sum 等文件,必须连同原因一起进入评审,不能在清理阶段自动丢弃。
密钥则不要复制进 Worktree。可以由任务执行器按需注入环境变量,并为测试账号设置最小权限。智能体生成的 .env、日志和临时凭据应在任务结束时删除,同时通过 .gitignore 防止误提交。
四、先验收候选合并,再写入主分支
要求智能体以提交作为交付边界:工作区必须干净,提交信息包含任务编号,并附上实际执行过的测试命令。协调器先检查 git diff main...agent/184-user-cache,再在临时 Worktree 中执行一次真实合并。这样既能检测文本冲突,也能测试“合并后的整体”,而不是只测试任务分支。
下面的脚本使用目录锁串行化合并,并在临时 Worktree 中预演。VERIFY_CMD 可以替换为项目自己的测试命令:
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "用法: $0 <候选分支> [目标分支]" >&2
exit 2
fi
branch="$1"
target="${2:-main}"
verify_cmd="${VERIFY_CMD:-git diff --cached --check}"
root="$(git rev-parse --show-toplevel)"
cd "$root"
[[ "$(git branch --show-current)" == "$target" ]] || {
echo "当前工作区必须位于目标分支 $target" >&2
exit 1
}
[[ -z "$(git status --porcelain)" ]] || {
echo "目标工作区存在未提交变更" >&2
exit 1
}
git rev-parse --verify "${branch}^{commit}" >/dev/null
common_dir="$(git rev-parse --git-common-dir)"
lock_dir="$common_dir/agent-integration.lock"
mkdir "$lock_dir" 2>/dev/null || {
echo "已有合并任务正在运行" >&2
exit 1
}
tmp="$(mktemp -d "${TMPDIR:-/tmp}/agent-merge.XXXXXX")"
registered=0
cleanup() {
if [[ "$registered" -eq 1 ]]; then
git worktree remove --force "$tmp" >/dev/null 2>&1 || true
fi
rm -rf "$tmp"
rmdir "$lock_dir" >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM
git worktree add --detach "$tmp" "$target"
registered=1
if ! git -C "$tmp" merge --no-commit --no-ff "$branch"; then
echo "检测到合并冲突:" >&2
git -C "$tmp" diff --name-only --diff-filter=U >&2 || true
exit 3
fi
(cd "$tmp" && bash -lc "$verify_cmd")
git worktree remove --force "$tmp"
registered=0
rm -rf "$tmp"
git merge --no-ff "$branch" -m "merge: $branch"
echo "已合并 $branch 到 $target"
例如:
VERIFY_CMD='npm ci && npm test' ./scripts/integrate.sh agent/184-user-cache main
目录锁只约束遵守这套脚本的本机进程;如果合并发生在多台机器,应改由 CI 的并发组、队列或受保护分支保证串行。合并失败时不要让原智能体直接操作主工作区,而应让它在自己的 Worktree 中合入最新目标分支、解决冲突、重新测试并提交。
五、回收失败任务与残留状态
失败任务不能简单执行 rm -rf。Git 仍会记录 Worktree,分支也可能保留有价值的提交。先分类处理:可重试任务保留分支和目录;已有部分成果的任务可推送分支或生成补丁;确认无价值后才强制删除。
正常回收已合并任务可执行:
git worktree remove ../my-repo-worktrees/184-user-cache
git branch -d agent/184-user-cache
git worktree prune
git branch -d 会拒绝删除未合并分支,这是有用的保护。只有人工确认可以丢弃时才使用 git worktree remove --force 和 git branch -D。如果目录中有未提交成果,可先保存补丁:
git -C ../my-repo-worktrees/184-user-cache diff --binary > task-184.patch
git -C ../my-repo-worktrees/184-user-cache status --short
补丁不包含未跟踪文件,因此还应检查 status 输出。定时任务可以运行 git worktree prune --dry-run 观察可清理记录,再按任务状态执行实际清理,不要仅根据目录年龄删除正在运行的任务。
总结
用 Git Worktree 调度多个代码智能体,关键不只是“多开几个目录”,而是明确一套可审计的生命周期:
- 使用任务编号命名分支和目录,并记录创建时的基线提交;
- 隔离工作区、索引和可变依赖,只复用工具下载缓存;
- 要求智能体以干净工作区和提交作为交付边界;
- 在临时 Worktree 中预演合并并执行验收,再串行写入目标分支;
- 冲突返回原任务工作区处理,失败任务在确认或归档后再回收;
- 用受保护分支和 CI 队列补足单机目录锁无法覆盖的并发场景。
这套设计不会消除冲突,但能让覆盖、污染和失败变得可检测、可定位、可回收,为后续接入智能体调度器保留清晰边界。
评论