Worktree sandbox 的目标不是“让 agent 随便改,最后再看运气合并”。它的目标是把每个任务放进独立目录、独立分支、独立依赖和独立测试证据里,让并行 agent 不互相污染主工作区。
本课的核心模型是:主仓库只负责分派任务和收结果;每个任务创建一个临时 worktree;agent 在 worktree 内安装依赖、修改代码、运行测试、生成 diff;主流程只合并通过验证且可审查的结果;任务结束后按状态清理或保留现场。
第 19 课解决的是“单个 agent 能做什么”。现在问题变成“多个 agent 同时做事时,如何不互相踩文件”:
- 三个 bugfix agent 如果都在同一个工作区运行,谁负责
node_modules、锁文件、测试缓存和未提交 diff? - 一个 agent 改坏了仓库,另一个 agent 的测试结果还可信吗?
- 子任务成功后,怎么把结果带回主分支?失败后又怎么保留证据?
本课从 /Users/ryanchen/codespace/pi-agent-course-lab/src/10-worktree-sandbox.ts 出发,把 git worktree add 扩展成一条完整的任务隔离链路。
把 worktree sandbox 看成一个任务容器,但它不是 Docker 容器,而是 Git 级别的隔离执行目录。
任务进入系统后会变成:
Task -> branch name -> temp root -> worktree path -> dependency install -> agent execution -> test evidence -> diff summary -> merge or archive -> cleanup这里最重要的变量是 task.id、task.branch、worktree.path 和 baseRef。task.id 用来命名日志和临时目录;task.branch 用来承载最终 diff;worktree.path 是所有工具执行的 cwd;baseRef 决定它从哪个提交开始工作。
1. 从 starter 的 planWorktree 读变量流
Section titled “1. 从 starter 的 planWorktree 读变量流”打开 /Users/ryanchen/codespace/pi-agent-course-lab/src/10-worktree-sandbox.ts。planWorktree(task) 做了三件事:
- 用
mkdtemp(join(tmpdir(), "forge-code-"))创建一个临时根目录。 - 把
task.id拼成具体 worktree path。 - 返回一组命令:
git worktree add -b、npm install、npm test、git diff --stat。
这只是计划,不直接执行。这样的设计适合教学:先让学生看清楚任务如何变成目录和命令,再讨论每一步的风险。
2. 创建 worktree 前先检查主工作区
Section titled “2. 创建 worktree 前先检查主工作区”真实系统创建 worktree 前要检查:
- 主仓库是否是 Git repo。
- base branch 是否存在。
task.branch是否已经存在。- 主工作区是否有未提交改动需要保护。
- 临时目录是否在允许路径下。
这一步属于 orchestrator,而不是交给 agent 自己判断。agent 的 cwd 应该已经是隔离 worktree。
3. 依赖安装要隔离记录
Section titled “3. 依赖安装要隔离记录”worktree 会共享 Git object database,但不会共享普通文件。node_modules、测试缓存、构建产物都在各自 worktree 中生成。依赖安装要记录命令、退出码和耗时,因为它会影响后续测试结果。
如果课程项目要省时间,可以先支持两种策略:
reuse:如果 repo 支持 pnpm/yarn workspace 缓存,复用全局 store。fresh:在 worktree 内运行npm install或npm ci,得到最干净但更慢的环境。
4. 测试隔离和证据收集
Section titled “4. 测试隔离和证据收集”测试命令必须以 worktree path 为 cwd。证据至少包含:
- 实际运行的命令。
- cwd。
- exit code。
- stdout/stderr 摘要。
- 失败时的完整日志路径。
git diff --stat和关键 diff。
这样主 agent 汇总结果时,不需要相信子 agent 的口头结论。
5. 合并结果要经过检查点
Section titled “5. 合并结果要经过检查点”成功的任务不能直接写回主工作区。更稳的流程是:
worktree branch -> collect diff -> run tests -> reviewer 检查 -> user or main agent approve -> merge/cherry-pick/apply patch如果多个 worktree 都修改同一文件,合并阶段才处理冲突。冲突不是 worktree 的失败,而是并行任务正常会遇到的结果。
6. 清理策略按状态区分
Section titled “6. 清理策略按状态区分”清理不能一律 rm -rf。建议按任务状态处理:
success + merged:删除 worktree,删除临时目录,可选择删除分支。success + not merged:保留 branch,删除临时目录前保存 patch 和日志。failed:默认保留 worktree 或至少保留日志、diff、错误摘要。aborted:保留现场,方便排查用户中断前做了什么。
本课实践基于 /Users/ryanchen/codespace/pi-agent-course-lab/src/10-worktree-sandbox.ts。先运行 starter,观察生成的 JSON:
{ "path": "/tmp/forge-code-.../task-login-test", "commands": [ ["git", "worktree", "add", "-b", "agent/task-login-test", "..."], ["npm", "install"], ["npm", "test"], ["git", "diff", "--stat"] ]}然后把它升级为带状态的计划对象:
type SandboxPlan = { taskId: string; branch: string; path: string; steps: Array<{ id: "create" | "install" | "run-agent" | "test" | "diff" | "cleanup"; command?: string[]; cwd: string; checkpoint: string; }>;};变量流很清楚:task 进入 planWorktree();root 决定临时目录;path 成为所有命令的 cwd;每个 step 都带 checkpoint,让 orchestrator 知道失败时停在哪里。
课堂代码推演
Section titled “课堂代码推演”课堂上把 worktree sandbox 压缩成一段可执行心智模型:
async function runTaskInWorktree(task) { const plan = await planWorktree(task);
await exec(["git", "worktree", "add", "-b", task.branch, plan.path], { cwd: repoRoot }); await exec(["npm", "install"], { cwd: plan.path });
const agentResult = await runAgent(task.prompt, { cwd: plan.path }); const testResult = await exec(["npm", "test"], { cwd: plan.path }); const diff = await exec(["git", "diff", "--stat"], { cwd: plan.path });
return { taskId: task.id, branch: task.branch, path: plan.path, ok: testResult.exitCode === 0, agentResult, testResult, diff, };}这段推演的重点不是命令本身,而是 cwd 的流动:git worktree add 在主仓库执行;依赖安装、agent、测试、diff 都在 plan.path 执行。如果测试命令不带 cwd,就会错误地测到主工作区。
再推一遍合并控制点:
if (result.ok && reviewer.approved) { await exec(["git", "merge", "--no-ff", result.branch], { cwd: repoRoot }); await cleanup(result.path, { keepLogs: true });} else { await archive(result.path, result.taskId);}学生要带走的判断是:worktree 负责隔离修改,review/test/checkpoint 负责决定能不能回到主线,cleanup 只在证据保存之后发生。
/Users/ryanchen/codespace/pi-agent-course-lab/src/10-worktree-sandbox.ts:本课 lab starter,展示从WorktreeTask到临时目录、branch 和命令列表的转换。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/bash.tsat61babc2:bash 工具通过 cwd 执行命令,支持 timeout、abort、输出截断和可替换 operations,是 worktree 内执行测试的基础边界。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session-services.tsat61babc2:createAgentSessionServices()以 cwd 创建 cwd-bound services,说明每个 worktree 应该有独立 runtime 视角。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session-runtime.tsat61babc2:runtime factory 和 session replacement 可以为不同 cwd 重建 session 宿主。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/sandbox/index.tsat61babc2:sandbox extension 展示如何把 bash operations 包进 OS-level sandbox,并配置网络和文件系统限制。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/bun/restore-sandbox-env.tsat61babc2:说明 sandbox 环境会影响进程环境变量,运行时需要考虑环境恢复。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/subagent/index.tsat61babc2:subagent 进程支持传入 cwd,可用于后续把子 agent 派到某个 worktree。
学完本课后,你应该能做到:
- 解释 git worktree 为什么适合并行 coding agent,而不是让多个任务共用一个工作区。
- 写出从
task.id、task.branch到worktree.path的计划生成逻辑。 - 区分主仓库 cwd 和 worktree cwd,知道哪些命令必须在哪个 cwd 执行。
- 设计依赖安装、测试执行、diff 收集、结果合并和清理策略。
- 说明失败任务为什么要保留证据,而不是直接删除临时目录。
- 实践题:扩展
/Users/ryanchen/codespace/pi-agent-course-lab/src/10-worktree-sandbox.ts,给每个 command 增加cwd、checkpoint和cleanupPolicy字段,并输出一个失败时可恢复的计划 JSON。 - 思考题:如果三个 worktree 都通过测试,但两个任务改了同一个文件,主 agent 应该在创建阶段、测试阶段还是合并阶段处理冲突?为什么?